mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge pull request #1065 from acelaya-forks/feature/split-db-update-and-location
Feature/split db update and location
This commit is contained in:
commit
f30e922074
40 changed files with 642 additions and 362 deletions
|
@ -5,7 +5,7 @@ data/log/*
|
|||
data/locks/*
|
||||
data/proxies/*
|
||||
data/migrations_template.txt
|
||||
data/GeoLite2-City.*
|
||||
data/GeoLite2-City*
|
||||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
CHANGELOG.md
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -6,8 +6,7 @@ composer.phar
|
|||
vendor/
|
||||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
data/GeoLite2-City.mmdb
|
||||
data/GeoLite2-City.mmdb.*
|
||||
data/GeoLite2-City.*
|
||||
docs/swagger-ui*
|
||||
docs/mercure.html
|
||||
docker-compose.override.yml
|
||||
|
|
|
@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* [#1044](https://github.com/shlinkio/shlink/issues/1044) Added ability to set names on API keys, which help identifying them when the list grows.
|
||||
* [#1044](https://github.com/shlinkio/shlink/issues/1044) Added ability to set names on API keys, which helps to identify them when the list grows.
|
||||
* [#819](https://github.com/shlinkio/shlink/issues/819) Visits are now always located in real time, even when not using swoole.
|
||||
|
||||
The only side effect is that a GeoLite2 db file is now installed when the docker image starts or during shlink installation or update.
|
||||
|
||||
Also, when using swoole, the file is now updated **after** tracking a visit, which means it will not apply until the next one.
|
||||
|
||||
### Changed
|
||||
* [#1036](https://github.com/shlinkio/shlink/issues/1036) Updated to `happyr/doctrine-specification` 2.0.
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"shlinkio/shlink-config": "^1.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.1",
|
||||
"shlinkio/shlink-importer": "^2.2",
|
||||
"shlinkio/shlink-installer": "^5.4",
|
||||
"shlinkio/shlink-installer": "dev-develop#aa50ea9 as 5.5",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.5",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/filesystem": "^5.1",
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Option;
|
||||
use Shlinkio\Shlink\Installer\Util\InstallationCommand;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -45,11 +48,14 @@ return [
|
|||
],
|
||||
|
||||
'installation_commands' => [
|
||||
'db_create_schema' => [
|
||||
'command' => 'bin/cli db:create',
|
||||
InstallationCommand::DB_CREATE_SCHEMA => [
|
||||
'command' => 'bin/cli ' . Command\Db\CreateDatabaseCommand::NAME,
|
||||
],
|
||||
'db_migrate' => [
|
||||
'command' => 'bin/cli db:migrate',
|
||||
InstallationCommand::DB_MIGRATE => [
|
||||
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
||||
],
|
||||
InstallationCommand::GEOLITE_DOWNLOAD_DB => [
|
||||
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
|
@ -170,7 +170,7 @@ return [
|
|||
],
|
||||
|
||||
'geolite2' => [
|
||||
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'),
|
||||
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove the default value
|
||||
],
|
||||
|
||||
'mercure' => $helper->getMercureConfig(),
|
||||
|
|
|
@ -15,6 +15,12 @@ php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q
|
|||
echo "Clearing entities cache..."
|
||||
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n -q
|
||||
|
||||
# Try to download GeoLite2 db file only if the license key env var was defined
|
||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
||||
echo "Downloading GeoLite2 db file..."
|
||||
php bin/cli visit:download-db -n -q
|
||||
fi
|
||||
|
||||
# When restarting the container, swoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
|
|
80
module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
Normal file
80
module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
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 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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DownloadGeoLiteDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:download-db';
|
||||
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private ?ProgressBar $progressBar = null;
|
||||
|
||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->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('<fg=blue>%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) {
|
||||
$io->info('GeoLite2 db file is up to date.');
|
||||
} else {
|
||||
$this->progressBar->finish();
|
||||
$io->success('GeoLite2 db file properly downloaded.');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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('<fg=blue>%s GeoLite2 database...</>', $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(
|
||||
'<fg=yellow;options=bold>[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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,11 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||
{
|
||||
private bool $olderDbExists;
|
||||
|
||||
private function __construct(string $message, int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function withOlderDb(?Throwable $prev = null): self
|
||||
{
|
||||
$e = new self(
|
||||
|
|
|
@ -32,13 +32,13 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(?callable $mustBeUpdated = null, ?callable $handleProgress = null): void
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void
|
||||
{
|
||||
$lock = $this->locker->createLock(self::LOCK_NAME);
|
||||
$lock->acquire(true); // Block until lock is released
|
||||
|
||||
try {
|
||||
$this->downloadIfNeeded($mustBeUpdated, $handleProgress);
|
||||
$this->downloadIfNeeded($beforeDownload, $handleProgress);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
|
@ -47,34 +47,16 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadIfNeeded(?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): void
|
||||
{
|
||||
if (! $this->dbUpdater->databaseFileExists()) {
|
||||
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
||||
$this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = $this->geoLiteDbReader->metadata();
|
||||
if ($this->buildIsTooOld($meta)) {
|
||||
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadNewDb(bool $olderDbExists, ?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||
{
|
||||
if ($mustBeUpdated !== null) {
|
||||
$mustBeUpdated($olderDbExists);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbUpdater->downloadFreshCopy($handleProgress);
|
||||
} catch (RuntimeException $e) {
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
||||
$this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,4 +87,31 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
|
||||
throw GeolocationDbUpdateFailedException::withInvalidEpochInOldDb($buildEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void
|
||||
{
|
||||
if ($beforeDownload !== null) {
|
||||
$beforeDownload($olderDbExists);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||
} catch (RuntimeException $e) {
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function wrapHandleProgressCallback(?callable $handleProgress, bool $olderDbExists): ?callable
|
||||
{
|
||||
if ($handleProgress === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,5 +11,5 @@ interface GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(?callable $mustBeUpdated = null, ?callable $handleProgress = null): void;
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void;
|
||||
}
|
||||
|
|
44
module/CLI/test/CliTestUtilsTrait.php
Normal file
44
module/CLI/test/CliTestUtilsTrait.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
trait CliTestUtilsTrait
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @return ObjectProphecy|Command
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
private function testerForCommand(Command $mainCommand, Command ...$extraCommands): CommandTester
|
||||
{
|
||||
$app = new Application();
|
||||
$app->add($mainCommand);
|
||||
foreach ($extraCommands as $command) {
|
||||
$app->add($command);
|
||||
}
|
||||
|
||||
return new CommandTester($mainCommand);
|
||||
}
|
||||
}
|
|
@ -5,17 +5,16 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
@ -23,10 +22,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -7,34 +7,30 @@ namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
|||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
private ObjectProphecy $roleResolver;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$this->roleResolver = $this->prophesize(RoleResolverInterface::class);
|
||||
$this->roleResolver->determineRoles(Argument::type(InputInterface::class))->willReturn([]);
|
||||
$roleResolver = $this->prophesize(RoleResolverInterface::class);
|
||||
$roleResolver->determineRoles(Argument::type(InputInterface::class))->willReturn([]);
|
||||
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), $this->roleResolver->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), $roleResolver->reveal());
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
@ -13,12 +12,12 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
@ -26,10 +25,7 @@ class ListKeysCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,11 +9,10 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
|
|||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
@ -22,7 +21,7 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
|||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $processHelper;
|
||||
|
@ -59,10 +58,8 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
$this->regularConn->reveal(),
|
||||
$noDbNameConn->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -6,11 +6,10 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
@ -19,7 +18,7 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
|||
|
||||
class MigrateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $processHelper;
|
||||
|
@ -43,10 +42,7 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||
$this->processHelper->reveal(),
|
||||
$phpExecutableFinder->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,18 +5,17 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListDomainsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $domainService;
|
||||
|
@ -24,12 +23,7 @@ class ListDomainsCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
||||
|
||||
$command = new ListDomainsCommand($this->domainService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -6,13 +6,12 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function array_pop;
|
||||
|
@ -22,7 +21,7 @@ use const PHP_EOL;
|
|||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $service;
|
||||
|
@ -30,12 +29,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
|
@ -17,12 +16,12 @@ use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
|||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GenerateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $urlShortener;
|
||||
|
@ -35,9 +34,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
$this->stringifier->stringify(Argument::type(ShortUrl::class))->willReturn('');
|
||||
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), $this->stringifier->reveal(), 5);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -8,7 +8,6 @@ use Cake\Chronos\Chronos;
|
|||
use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
|
@ -21,14 +20,14 @@ use Shlinkio\Shlink\Core\Model\Visitor;
|
|||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitsHelper;
|
||||
|
@ -37,9 +36,7 @@ class GetVisitsCommandTest extends TestCase
|
|||
{
|
||||
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||
$command = new GetVisitsCommand($this->visitsHelper->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -8,7 +8,6 @@ use Cake\Chronos\Chronos;
|
|||
use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
|
@ -17,14 +16,14 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
|||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function explode;
|
||||
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $shortUrlService;
|
||||
|
@ -32,12 +31,10 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), new ShortUrlDataTransformer(
|
||||
new ShortUrlStringifier([]),
|
||||
));
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,14 +5,13 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
@ -21,7 +20,7 @@ use const PHP_EOL;
|
|||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $urlResolver;
|
||||
|
@ -29,11 +28,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||
$command = new ResolveUrlCommand($this->urlResolver->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -6,16 +6,15 @@ namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
|||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
@ -23,12 +22,7 @@ class CreateTagCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new CreateTagCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new CreateTagCommand($this->tagService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,16 +5,15 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
@ -22,12 +21,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new DeleteTagsCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,18 +5,17 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListTagsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
@ -24,12 +23,7 @@ class ListTagsCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new ListTagsCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -5,19 +5,18 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class RenameTagCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
@ -25,12 +24,7 @@ class RenameTagCommandTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new RenameTagCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
107
module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php
Normal file
107
module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DownloadGeoLiteDbCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFailureParams
|
||||
*/
|
||||
public function showsProperMessageWhenGeoLiteUpdateFails(
|
||||
bool $olderDbExists,
|
||||
string $expectedMessage,
|
||||
int $expectedExitCode
|
||||
): void {
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
function (array $args) use ($olderDbExists): void {
|
||||
[$beforeDownload, $handleProgress] = $args;
|
||||
|
||||
$beforeDownload($olderDbExists);
|
||||
$handleProgress(100, 50);
|
||||
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb()
|
||||
: GeolocationDbUpdateFailedException::withoutOlderDb();
|
||||
},
|
||||
);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString(
|
||||
sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
$output,
|
||||
);
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
self::assertSame($expectedExitCode, $exitCode);
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideFailureParams(): iterable
|
||||
{
|
||||
yield 'existing db' => [
|
||||
true,
|
||||
'[WARNING] GeoLite2 db file update failed. Visits will continue to be located',
|
||||
ExitCodes::EXIT_WARNING,
|
||||
];
|
||||
yield 'not existing db' => [
|
||||
false,
|
||||
'[ERROR] GeoLite2 db file download failed. It will not be possible to locate',
|
||||
ExitCodes::EXIT_FAILURE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideSuccessParams
|
||||
*/
|
||||
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
|
||||
{
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will($checkUpdateBehavior);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideSuccessParams(): iterable
|
||||
{
|
||||
yield 'up to date db' => [function (): void {
|
||||
}, '[INFO] GeoLite2 db file is up to date.'];
|
||||
yield 'outdated db' => [function (array $args): void {
|
||||
[$beforeDownload] = $args;
|
||||
$beforeDownload(true);
|
||||
}, '[OK] GeoLite2 db file properly downloaded.'];
|
||||
}
|
||||
}
|
|
@ -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,7 +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 Symfony\Component\Console\Application;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
@ -33,19 +32,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,12 +56,12 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
$locker->reveal(),
|
||||
$this->dbUpdater->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->downloadDbCommand = $this->createCommandMock(DownloadGeoLiteDbCommand::NAME);
|
||||
$this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,43 +200,16 @@ 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 database...', $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.'];
|
||||
self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output);
|
||||
$this->visitService->locateUnlocatedVisits(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core;
|
|||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Symfony\Component\Mercure\Hub;
|
||||
|
||||
|
@ -14,14 +15,15 @@ return [
|
|||
|
||||
'events' => [
|
||||
'regular' => [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\NotifyVisitToMercure::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
],
|
||||
'async' => [
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\NotifyVisitToMercure::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
@ -31,10 +33,14 @@ return [
|
|||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
EventDispatcher\LocateVisit::class => [
|
||||
EventDispatcher\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
],
|
||||
|
@ -45,7 +51,7 @@ return [
|
|||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
GeolocationDbUpdater::class,
|
||||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
|
@ -62,6 +68,7 @@ return [
|
|||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -7,31 +7,29 @@ namespace Shlinkio\Shlink\Core\EventDispatcher;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
use function sprintf;
|
||||
use Throwable;
|
||||
|
||||
class LocateVisit
|
||||
{
|
||||
private IpLocationResolverInterface $ipLocationResolver;
|
||||
private EntityManagerInterface $em;
|
||||
private LoggerInterface $logger;
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private DbUpdaterInterface $dbUpdater;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
GeolocationDbUpdaterInterface $dbUpdater,
|
||||
DbUpdaterInterface $dbUpdater,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
|
@ -54,36 +52,19 @@ class LocateVisit
|
|||
return;
|
||||
}
|
||||
|
||||
if ($this->downloadOrUpdateGeoLiteDb($visitId)) {
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
|
||||
}
|
||||
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
|
||||
$this->eventDispatcher->dispatch(new VisitLocated($visitId));
|
||||
}
|
||||
|
||||
private function downloadOrUpdateGeoLiteDb(string $visitId): bool
|
||||
{
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists): void {
|
||||
$this->logger->notice(sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'));
|
||||
});
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
if (! $e->olderDbExists()) {
|
||||
$this->logger->error(
|
||||
'GeoLite2 database download failed. It is not possible to locate visit with id {visitId}. {e}',
|
||||
['e' => $e, 'visitId' => $visitId],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->logger->warning('GeoLite2 database update failed. Proceeding with old version. {e}', ['e' => $e]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function locateVisit(string $visitId, ?string $originalIpAddress, Visit $visit): void
|
||||
{
|
||||
if (! $this->dbUpdater->databaseFileExists()) {
|
||||
$this->logger->warning('Tried to locate visit with id "{visitId}", but a GeoLite2 db was not found.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$isLocatable = $originalIpAddress !== null || $visit->isLocatable();
|
||||
$addr = $originalIpAddress ?? $visit->getRemoteAddr();
|
||||
|
||||
|
@ -97,6 +78,11 @@ class LocateVisit
|
|||
'Tried to locate visit with id "{visitId}", but its address seems to be wrong. {e}',
|
||||
['e' => $e, 'visitId' => $visitId],
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'An unexpected error occurred while trying to locate visit with id "{visitId}". {e}',
|
||||
['e' => $e, 'visitId' => $visitId],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
45
module/Core/src/EventDispatcher/UpdateGeoLiteDb.php
Normal file
45
module/Core/src/EventDispatcher/UpdateGeoLiteDb.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class UpdateGeoLiteDb
|
||||
{
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater, LoggerInterface $logger)
|
||||
{
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function __invoke(): void
|
||||
{
|
||||
$beforeDownload = fn (bool $olderDbExists) => $this->logger->notice(
|
||||
sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
);
|
||||
$messageLogged = false;
|
||||
$handleProgress = function (int $total, int $downloaded, bool $olderDbExists) use (&$messageLogged): void {
|
||||
if ($messageLogged || $total > $downloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$messageLogged = true;
|
||||
$this->logger->notice(sprintf('Finished %s GeoLite2 db file', $olderDbExists ? 'updating' : 'downloading'));
|
||||
};
|
||||
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate($beforeDownload, $handleProgress);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('GeoLite2 database download failed. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -65,9 +65,11 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||
|
||||
private function trackVisit(Visit $visit, Visitor $visitor): void
|
||||
{
|
||||
$this->em->persist($visit);
|
||||
$this->em->flush();
|
||||
$this->em->transactional(function () use ($visit, $visitor): void {
|
||||
$this->em->persist($visit);
|
||||
$this->em->flush();
|
||||
|
||||
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->getRemoteAddress()));
|
||||
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->getRemoteAddress()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OutOfRangeException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
|
@ -22,6 +21,7 @@ use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
|||
use Shlinkio\Shlink\Core\EventDispatcher\LocateVisit;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
|
@ -41,9 +41,11 @@ class LocateVisitTest extends TestCase
|
|||
$this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
|
||||
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
$this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||
|
||||
$this->locateVisit = new LocateVisit(
|
||||
$this->ipLocationResolver->reveal(),
|
||||
$this->em->reveal(),
|
||||
|
@ -73,6 +75,31 @@ class LocateVisitTest extends TestCase
|
|||
$dispatch->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function nonExistingGeoLiteDbLogsWarning(): void
|
||||
{
|
||||
$event = new UrlVisited('123');
|
||||
$findVisit = $this->em->find(Visit::class, '123')->willReturn(
|
||||
Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', '')),
|
||||
);
|
||||
$dbExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||
$logWarning = $this->logger->warning(
|
||||
'Tried to locate visit with id "{visitId}", but a GeoLite2 db was not found.',
|
||||
['visitId' => 123],
|
||||
);
|
||||
$dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
|
||||
});
|
||||
|
||||
($this->locateVisit)($event);
|
||||
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$dbExists->shouldHaveBeenCalledOnce();
|
||||
$this->em->flush()->shouldNotHaveBeenCalled();
|
||||
$this->ipLocationResolver->resolveIpLocation(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$logWarning->shouldHaveBeenCalled();
|
||||
$dispatch->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function invalidAddressLogsWarning(): void
|
||||
{
|
||||
|
@ -84,7 +111,7 @@ class LocateVisitTest extends TestCase
|
|||
WrongIpException::class,
|
||||
);
|
||||
$logWarning = $this->logger->warning(
|
||||
Argument::containingString('Tried to locate visit with id "{visitId}", but its address seems to be wrong.'),
|
||||
'Tried to locate visit with id "{visitId}", but its address seems to be wrong. {e}',
|
||||
Argument::type('array'),
|
||||
);
|
||||
$dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
|
||||
|
@ -99,6 +126,32 @@ class LocateVisitTest extends TestCase
|
|||
$dispatch->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function unhandledExceptionLogsError(): void
|
||||
{
|
||||
$event = new UrlVisited('123');
|
||||
$findVisit = $this->em->find(Visit::class, '123')->willReturn(
|
||||
Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', '')),
|
||||
);
|
||||
$resolveLocation = $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow(
|
||||
OutOfRangeException::class,
|
||||
);
|
||||
$logError = $this->logger->error(
|
||||
'An unexpected error occurred while trying to locate visit with id "{visitId}". {e}',
|
||||
Argument::type('array'),
|
||||
);
|
||||
$dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
|
||||
});
|
||||
|
||||
($this->locateVisit)($event);
|
||||
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$resolveLocation->shouldHaveBeenCalledOnce();
|
||||
$logError->shouldHaveBeenCalled();
|
||||
$this->em->flush()->shouldNotHaveBeenCalled();
|
||||
$dispatch->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideNonLocatableVisits
|
||||
|
@ -173,67 +226,4 @@ class LocateVisitTest extends TestCase
|
|||
yield 'invalid short url' => [Visit::forInvalidShortUrl(new Visitor('', '', '1.2.3.4', '')), '1.2.3.4'];
|
||||
yield 'regular not found' => [Visit::forRegularNotFound(new Visitor('', '', '1.2.3.4', '')), '1.2.3.4'];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function errorWhenUpdatingGeoLiteWithExistingCopyLogsWarning(): void
|
||||
{
|
||||
$e = GeolocationDbUpdateFailedException::withOlderDb();
|
||||
$ipAddr = '1.2.3.0';
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr, ''));
|
||||
$location = new Location('', '', '', '', 0.0, 0.0, '');
|
||||
$event = new UrlVisited('123');
|
||||
|
||||
$findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
|
||||
$flush = $this->em->flush()->will(function (): void {
|
||||
});
|
||||
$resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
|
||||
$checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
|
||||
$dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
|
||||
});
|
||||
|
||||
($this->locateVisit)($event);
|
||||
|
||||
self::assertEquals($visit->getVisitLocation(), new VisitLocation($location));
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$flush->shouldHaveBeenCalledOnce();
|
||||
$resolveIp->shouldHaveBeenCalledOnce();
|
||||
$checkUpdateDb->shouldHaveBeenCalledOnce();
|
||||
$this->logger->warning(
|
||||
'GeoLite2 database update failed. Proceeding with old version. {e}',
|
||||
['e' => $e],
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
$dispatch->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function errorWhenDownloadingGeoLiteCancelsLocation(): void
|
||||
{
|
||||
$e = GeolocationDbUpdateFailedException::withoutOlderDb();
|
||||
$ipAddr = '1.2.3.0';
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr, ''));
|
||||
$location = new Location('', '', '', '', 0.0, 0.0, '');
|
||||
$event = new UrlVisited('123');
|
||||
|
||||
$findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
|
||||
$flush = $this->em->flush()->will(function (): void {
|
||||
});
|
||||
$resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
|
||||
$checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
|
||||
$logError = $this->logger->error(
|
||||
'GeoLite2 database download failed. It is not possible to locate visit with id {visitId}. {e}',
|
||||
['e' => $e, 'visitId' => 123],
|
||||
);
|
||||
$dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
|
||||
});
|
||||
|
||||
($this->locateVisit)($event);
|
||||
|
||||
self::assertNull($visit->getVisitLocation());
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$flush->shouldNotHaveBeenCalled();
|
||||
$resolveIp->shouldNotHaveBeenCalled();
|
||||
$checkUpdateDb->shouldHaveBeenCalledOnce();
|
||||
$logError->shouldHaveBeenCalledOnce();
|
||||
$dispatch->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
118
module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php
Normal file
118
module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
|
||||
|
||||
class UpdateGeoLiteDbTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private UpdateGeoLiteDb $listener;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
private ObjectProphecy $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||
|
||||
$this->listener = new UpdateGeoLiteDb($this->dbUpdater->reveal(), $this->logger->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function exceptionWhileUpdatingDbLogsError(): void
|
||||
{
|
||||
$e = new RuntimeException();
|
||||
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
|
||||
$logError = $this->logger->error('GeoLite2 database download failed. {e}', ['e' => $e]);
|
||||
|
||||
($this->listener)();
|
||||
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
$logError->shouldHaveBeenCalledOnce();
|
||||
$this->logger->notice(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFlags
|
||||
*/
|
||||
public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void
|
||||
{
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
function (array $args) use ($oldDbExists): void {
|
||||
[$firstCallback] = $args;
|
||||
$firstCallback($oldDbExists);
|
||||
},
|
||||
);
|
||||
$logNotice = $this->logger->notice($expectedMessage);
|
||||
|
||||
($this->listener)();
|
||||
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
$logNotice->shouldHaveBeenCalledOnce();
|
||||
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideFlags(): iterable
|
||||
{
|
||||
yield 'existing old db' => [true, 'Updating GeoLite2 db file...'];
|
||||
yield 'not existing old db' => [false, 'Downloading GeoLite2 db file...'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideDownloaded
|
||||
*/
|
||||
public function noticeMessageIsPrintedWhenSecondCallbackIsInvoked(
|
||||
int $total,
|
||||
int $downloaded,
|
||||
bool $oldDbExists,
|
||||
?string $expectedMessage
|
||||
): void {
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
function (array $args) use ($total, $downloaded, $oldDbExists): void {
|
||||
[, $secondCallback] = $args;
|
||||
|
||||
// Invoke several times to ensure the log is printed only once
|
||||
$secondCallback($total, $downloaded, $oldDbExists);
|
||||
$secondCallback($total, $downloaded, $oldDbExists);
|
||||
$secondCallback($total, $downloaded, $oldDbExists);
|
||||
},
|
||||
);
|
||||
$logNotice = $this->logger->notice($expectedMessage ?? Argument::cetera());
|
||||
|
||||
($this->listener)();
|
||||
|
||||
if ($expectedMessage !== null) {
|
||||
$logNotice->shouldHaveBeenCalledOnce();
|
||||
} else {
|
||||
$logNotice->shouldNotHaveBeenCalled();
|
||||
}
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideDownloaded(): iterable
|
||||
{
|
||||
yield [100, 0, true, null];
|
||||
yield [100, 0, false, null];
|
||||
yield [100, 99, true, null];
|
||||
yield [100, 99, false, null];
|
||||
yield [100, 100, true, 'Finished updating GeoLite2 db file'];
|
||||
yield [100, 100, false, 'Finished downloading GeoLite2 db file'];
|
||||
yield [100, 101, true, 'Finished updating GeoLite2 db file'];
|
||||
yield [100, 101, false, 'Finished downloading GeoLite2 db file'];
|
||||
}
|
||||
}
|
|
@ -29,6 +29,11 @@ class VisitsTrackerTest extends TestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
$this->em = $this->prophesize(EntityManager::class);
|
||||
$this->em->transactional(Argument::any())->will(function (array $args) {
|
||||
[$callback] = $args;
|
||||
return $callback();
|
||||
});
|
||||
|
||||
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
|
||||
$this->options = new UrlShortenerOptions();
|
||||
|
||||
|
@ -41,11 +46,14 @@ class VisitsTrackerTest extends TestCase
|
|||
*/
|
||||
public function trackPersistsVisitAndDispatchesEvent(string $method, array $args): void
|
||||
{
|
||||
$this->em->persist(Argument::that(fn (Visit $visit) => $visit->setId('1')))->shouldBeCalledOnce();
|
||||
$this->em->flush()->shouldBeCalledOnce();
|
||||
$persist = $this->em->persist(Argument::that(fn (Visit $visit) => $visit->setId('1')))->will(function (): void {
|
||||
});
|
||||
|
||||
$this->visitsTracker->{$method}(...$args);
|
||||
|
||||
$persist->shouldHaveBeenCalledOnce();
|
||||
$this->em->transactional(Argument::cetera())->shouldHaveBeenCalledOnce();
|
||||
$this->em->flush()->shouldHaveBeenCalledOnce();
|
||||
$this->eventDispatcher->dispatch(Argument::type(UrlVisited::class))->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
@ -68,6 +76,7 @@ class VisitsTrackerTest extends TestCase
|
|||
$this->visitsTracker->{$method}(Visitor::emptyInstance());
|
||||
|
||||
$this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->em->transactional(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->em->flush()->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue