mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Merge pull request #1548 from acelaya-forks/feature/deferred-geolite-download
Feature/deferred geolite download
This commit is contained in:
commit
53b9e3ddc1
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
## [Unreleased]
|
## [3.3.0] - 2022-09-18
|
||||||
### Added
|
### Added
|
||||||
* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole.
|
* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole.
|
||||||
* [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs.
|
* [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs.
|
||||||
@ -34,6 +34,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
|
|
||||||
Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars.
|
Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars.
|
||||||
|
|
||||||
|
* [#1528](https://github.com/shlinkio/shlink/issues/1528) Added support to delay when the GeoLite2 DB file is downloaded in docker images, speeding up its startup time.
|
||||||
|
|
||||||
|
In order to do it, pass `SKIP_INITIAL_GEOLITE_DOWNLOAD=true` when creating the container.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests.
|
* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests.
|
||||||
* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache.
|
* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache.
|
||||||
|
@ -45,12 +45,12 @@
|
|||||||
"php-middleware/request-id": "^4.1",
|
"php-middleware/request-id": "^4.1",
|
||||||
"pugx/shortid-php": "^1.0",
|
"pugx/shortid-php": "^1.0",
|
||||||
"ramsey/uuid": "^4.3",
|
"ramsey/uuid": "^4.3",
|
||||||
"shlinkio/shlink-common": "dev-main#c9e6474 as 5.1",
|
"shlinkio/shlink-common": "^5.1",
|
||||||
"shlinkio/shlink-config": "dev-main#12fb295 as 2.1",
|
"shlinkio/shlink-config": "^2.1",
|
||||||
"shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6",
|
"shlinkio/shlink-event-dispatcher": "^2.6",
|
||||||
"shlinkio/shlink-importer": "^4.0",
|
"shlinkio/shlink-importer": "^4.0",
|
||||||
"shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2",
|
"shlinkio/shlink-installer": "^8.2",
|
||||||
"shlinkio/shlink-ip-geolocation": "^3.0",
|
"shlinkio/shlink-ip-geolocation": "^3.1",
|
||||||
"spiral/roadrunner": "^2.11",
|
"spiral/roadrunner": "^2.11",
|
||||||
"spiral/roadrunner-jobs": "^2.3",
|
"spiral/roadrunner-jobs": "^2.3",
|
||||||
"symfony/console": "^6.1",
|
"symfony/console": "^6.1",
|
||||||
@ -73,7 +73,7 @@
|
|||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~2.3.0",
|
"shlinkio/php-coding-standard": "~2.3.0",
|
||||||
"shlinkio/shlink-test-utils": "dev-main#404fdf6 as 3.3",
|
"shlinkio/shlink-test-utils": "^3.3",
|
||||||
"symfony/var-dumper": "^6.1",
|
"symfony/var-dumper": "^6.1",
|
||||||
"veewee/composer-run-parallel": "^1.1"
|
"veewee/composer-run-parallel": "^1.1"
|
||||||
},
|
},
|
||||||
|
@ -18,14 +18,14 @@ php bin/doctrine orm:generate-proxies -n ${flags}
|
|||||||
echo "Clearing entities cache..."
|
echo "Clearing entities cache..."
|
||||||
php bin/doctrine orm:clear-cache:metadata -n ${flags}
|
php bin/doctrine orm:clear-cache:metadata -n ${flags}
|
||||||
|
|
||||||
# Try to download GeoLite2 db file only if the license key env var was defined
|
# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
|
||||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
|
||||||
echo "Downloading GeoLite2 db file..."
|
echo "Downloading GeoLite2 db file..."
|
||||||
php bin/cli visit:download-db -n ${flags}
|
php bin/cli visit:download-db -n ${flags}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
||||||
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
|
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
|
||||||
echo "Configuring periodic visit location..."
|
echo "Configuring periodic visit location..."
|
||||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||||
/usr/sbin/crond &
|
/usr/sbin/crond &
|
||||||
|
@ -19,7 +19,6 @@ use Shlinkio\Shlink\Core\Tag\TagService;
|
|||||||
use Shlinkio\Shlink\Core\Visit;
|
use Shlinkio\Shlink\Core\Visit;
|
||||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
use Symfony\Component\Console as SymfonyCli;
|
use Symfony\Component\Console as SymfonyCli;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
@ -35,7 +34,7 @@ return [
|
|||||||
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
|
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
|
||||||
PhpExecutableFinder::class => InvokableFactory::class,
|
PhpExecutableFinder::class => InvokableFactory::class,
|
||||||
|
|
||||||
Util\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||||
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
||||||
@ -70,7 +69,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
Util\GeolocationDbUpdater::class => [
|
GeoLite\GeolocationDbUpdater::class => [
|
||||||
DbUpdater::class,
|
DbUpdater::class,
|
||||||
Reader::class,
|
Reader::class,
|
||||||
LOCAL_LOCK_FACTORY,
|
LOCAL_LOCK_FACTORY,
|
||||||
@ -92,10 +91,10 @@ return [
|
|||||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||||
Command\Visit\LocateVisitsCommand::class => [
|
Command\Visit\LocateVisitsCommand::class => [
|
||||||
Visit\VisitLocator::class,
|
Visit\VisitLocator::class,
|
||||||
IpLocationResolverInterface::class,
|
Visit\VisitToLocationHelper::class,
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
],
|
],
|
||||||
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
@ -5,8 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
@ -11,11 +11,11 @@ use Shlinkio\Shlink\Common\Util\IpAddress;
|
|||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
|
||||||
use Symfony\Component\Console\Exception\RuntimeException;
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
use Symfony\Component\Console\Input\ArrayInput;
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@ -34,8 +34,8 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
private SymfonyStyle $io;
|
private SymfonyStyle $io;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private VisitLocatorInterface $visitLocator,
|
private readonly VisitLocatorInterface $visitLocator,
|
||||||
private IpLocationResolverInterface $ipLocationResolver,
|
private readonly VisitToLocationHelperInterface $visitToLocation,
|
||||||
LockFactory $locker,
|
LockFactory $locker,
|
||||||
) {
|
) {
|
||||||
parent::__construct($locker);
|
parent::__construct($locker);
|
||||||
@ -132,39 +132,33 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
*/
|
*/
|
||||||
public function geolocateVisit(Visit $visit): Location
|
public function geolocateVisit(Visit $visit): Location
|
||||||
{
|
{
|
||||||
if (! $visit->hasRemoteAddr()) {
|
$ipAddr = $visit->getRemoteAddr() ?? '?';
|
||||||
$this->io->writeln(
|
|
||||||
'<comment>Ignored visit with no IP address</comment>',
|
|
||||||
OutputInterface::VERBOSITY_VERBOSE,
|
|
||||||
);
|
|
||||||
throw IpCannotBeLocatedException::forEmptyAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
$ipAddr = $visit->getRemoteAddr() ?? '';
|
|
||||||
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
|
||||||
$this->io->writeln(' [<comment>Ignored localhost address</comment>]');
|
|
||||||
throw IpCannotBeLocatedException::forLocalhost();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
return $this->visitToLocation->resolveVisitLocation($visit);
|
||||||
} catch (WrongIpException $e) {
|
} catch (IpCannotBeLocatedException $e) {
|
||||||
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
$this->io->writeln(match ($e->type) {
|
||||||
if ($this->io->isVerbose()) {
|
UnlocatableIpType::EMPTY_ADDRESS => ' [<comment>Ignored visit with no IP address</comment>]',
|
||||||
|
UnlocatableIpType::LOCALHOST => ' [<comment>Ignored localhost address</comment>]',
|
||||||
|
UnlocatableIpType::ERROR => ' [<fg=red>An error occurred while locating IP. Skipped</>]',
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($e->type === UnlocatableIpType::ERROR && $this->io->isVerbose()) {
|
||||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IpCannotBeLocatedException::forError($e);
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
||||||
{
|
{
|
||||||
$message = ! $visitLocation->isEmpty()
|
if (! $visitLocation->isEmpty()) {
|
||||||
? sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName())
|
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName()));
|
||||||
: ' [<comment>Address not found</comment>]';
|
} elseif ($visit->hasRemoteAddr() && $visit->getRemoteAddr() !== IpAddress::LOCALHOST) {
|
||||||
$this->io->writeln($message);
|
$this->io->writeln(' <comment>[Could not locate address]</comment>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkDbUpdate(): void
|
private function checkDbUpdate(): void
|
||||||
|
@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Util;
|
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use MaxMind\Db\Reader\Metadata;
|
use MaxMind\Db\Reader\Metadata;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
|
|
||||||
@ -20,27 +22,27 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
private const LOCK_NAME = 'geolocation-db-update';
|
private const LOCK_NAME = 'geolocation-db-update';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DbUpdaterInterface $dbUpdater,
|
private readonly DbUpdaterInterface $dbUpdater,
|
||||||
private Reader $geoLiteDbReader,
|
private readonly Reader $geoLiteDbReader,
|
||||||
private LockFactory $locker,
|
private readonly LockFactory $locker,
|
||||||
private TrackingOptions $trackingOptions,
|
private readonly TrackingOptions $trackingOptions,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void
|
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): GeolocationResult
|
||||||
{
|
{
|
||||||
if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) {
|
if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) {
|
||||||
return;
|
return GeolocationResult::CHECK_SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lock = $this->locker->createLock(self::LOCK_NAME);
|
$lock = $this->locker->createLock(self::LOCK_NAME);
|
||||||
$lock->acquire(true); // Block until lock is released
|
$lock->acquire(true); // Block until lock is released
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->downloadIfNeeded($beforeDownload, $handleProgress);
|
return $this->downloadIfNeeded($beforeDownload, $handleProgress);
|
||||||
} finally {
|
} finally {
|
||||||
$lock->release();
|
$lock->release();
|
||||||
}
|
}
|
||||||
@ -49,17 +51,18 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): void
|
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult
|
||||||
{
|
{
|
||||||
if (! $this->dbUpdater->databaseFileExists()) {
|
if (! $this->dbUpdater->databaseFileExists()) {
|
||||||
$this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta = $this->geoLiteDbReader->metadata();
|
$meta = $this->geoLiteDbReader->metadata();
|
||||||
if ($this->buildIsTooOld($meta)) {
|
if ($this->buildIsTooOld($meta)) {
|
||||||
$this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
return $this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return GeolocationResult::DB_IS_UP_TO_DATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildIsTooOld(Metadata $meta): bool
|
private function buildIsTooOld(Metadata $meta): bool
|
||||||
@ -92,15 +95,22 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void
|
private function downloadNewDb(
|
||||||
{
|
bool $olderDbExists,
|
||||||
|
?callable $beforeDownload,
|
||||||
|
?callable $handleProgress,
|
||||||
|
): GeolocationResult {
|
||||||
if ($beforeDownload !== null) {
|
if ($beforeDownload !== null) {
|
||||||
$beforeDownload($olderDbExists);
|
$beforeDownload($olderDbExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||||
} catch (RuntimeException $e) {
|
return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
|
||||||
|
} catch (MissingLicenseException) {
|
||||||
|
// If there's no license key, just ignore the error
|
||||||
|
return GeolocationResult::CHECK_SKIPPED;
|
||||||
|
} catch (DbUpdateException | WrongIpException $e) {
|
||||||
throw $olderDbExists
|
throw $olderDbExists
|
||||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||||
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
||||||
@ -113,6 +123,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
return static fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Util;
|
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
|
||||||
@ -11,5 +11,8 @@ interface GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void;
|
public function checkDbUpdate(
|
||||||
|
?callable $beforeDownload = null,
|
||||||
|
?callable $handleProgress = null,
|
||||||
|
): GeolocationResult;
|
||||||
}
|
}
|
11
module/CLI/src/GeoLite/GeolocationResult.php
Normal file
11
module/CLI/src/GeoLite/GeolocationResult.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
|
enum GeolocationResult
|
||||||
|
{
|
||||||
|
case CHECK_SKIPPED;
|
||||||
|
case DB_CREATED;
|
||||||
|
case DB_UPDATED;
|
||||||
|
case DB_IS_UP_TO_DATE;
|
||||||
|
}
|
@ -9,8 +9,9 @@ use Prophecy\Argument;
|
|||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
|
||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
@ -97,11 +98,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
|
|
||||||
public function provideSuccessParams(): iterable
|
public function provideSuccessParams(): iterable
|
||||||
{
|
{
|
||||||
yield 'up to date db' => [function (): void {
|
yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
|
||||||
}, '[INFO] GeoLite2 db file is up to date.'];
|
yield 'outdated db' => [function (array $args): GeolocationResult {
|
||||||
yield 'outdated db' => [function (array $args): void {
|
|
||||||
[$beforeDownload] = $args;
|
[$beforeDownload] = $args;
|
||||||
$beforeDownload(true);
|
$beforeDownload(true);
|
||||||
|
|
||||||
|
return GeolocationResult::DB_CREATED;
|
||||||
}, '[OK] GeoLite2 db file properly downloaded.'];
|
}, '[OK] GeoLite2 db file properly downloaded.'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,16 +10,16 @@ use Prophecy\Prophecy\ObjectProphecy;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitLocator;
|
use Shlinkio\Shlink\Core\Visit\VisitLocator;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
|
||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
use Symfony\Component\Console\Exception\RuntimeException;
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@ -36,14 +36,14 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $visitService;
|
private ObjectProphecy $visitService;
|
||||||
private ObjectProphecy $ipResolver;
|
private ObjectProphecy $visitToLocation;
|
||||||
private ObjectProphecy $lock;
|
private ObjectProphecy $lock;
|
||||||
private ObjectProphecy $downloadDbCommand;
|
private ObjectProphecy $downloadDbCommand;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitService = $this->prophesize(VisitLocator::class);
|
$this->visitService = $this->prophesize(VisitLocator::class);
|
||||||
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
$this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class);
|
||||||
|
|
||||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||||
@ -54,7 +54,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$command = new LocateVisitsCommand(
|
$command = new LocateVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
$this->ipResolver->reveal(),
|
$this->visitToLocation->reveal(),
|
||||||
$locker->reveal(),
|
$locker->reveal(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$mockMethodBehavior,
|
$mockMethodBehavior,
|
||||||
);
|
);
|
||||||
$locateAllVisits = $this->visitService->locateAllVisits(Argument::cetera())->will($mockMethodBehavior);
|
$locateAllVisits = $this->visitService->locateAllVisits(Argument::cetera())->will($mockMethodBehavior);
|
||||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willReturn(
|
||||||
Location::emptyInstance(),
|
Location::emptyInstance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -117,36 +117,29 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
* @test
|
* @test
|
||||||
* @dataProvider provideIgnoredAddresses
|
* @dataProvider provideIgnoredAddresses
|
||||||
*/
|
*/
|
||||||
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
|
public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void
|
||||||
{
|
{
|
||||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $address, ''));
|
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance());
|
||||||
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
|
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
|
||||||
|
|
||||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
|
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
|
||||||
$this->invokeHelperMethods($visit, $location),
|
$this->invokeHelperMethods($visit, $location),
|
||||||
);
|
);
|
||||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow($e);
|
||||||
Location::emptyInstance(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
|
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
self::assertStringContainsString('Processing IP', $output);
|
||||||
self::assertStringContainsString($message, $output);
|
self::assertStringContainsString($message, $output);
|
||||||
if (empty($address)) {
|
|
||||||
self::assertStringNotContainsString('Processing IP', $output);
|
|
||||||
} else {
|
|
||||||
self::assertStringContainsString('Processing IP', $output);
|
|
||||||
}
|
|
||||||
$locateVisits->shouldHaveBeenCalledOnce();
|
$locateVisits->shouldHaveBeenCalledOnce();
|
||||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideIgnoredAddresses(): iterable
|
public function provideIgnoredAddresses(): iterable
|
||||||
{
|
{
|
||||||
yield 'with empty address' => ['', 'Ignored visit with no IP address'];
|
yield 'empty address' => [IpCannotBeLocatedException::forEmptyAddress(), 'Ignored visit with no IP address'];
|
||||||
yield 'with null address' => [null, 'Ignored visit with no IP address'];
|
yield 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address'];
|
||||||
yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@ -158,7 +151,9 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
|
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
|
||||||
$this->invokeHelperMethods($visit, $location),
|
$this->invokeHelperMethods($visit, $location),
|
||||||
);
|
);
|
||||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
|
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow(
|
||||||
|
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
|
||||||
|
);
|
||||||
|
|
||||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
|
|
||||||
@ -187,7 +182,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
|
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
|
||||||
});
|
});
|
||||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any());
|
||||||
|
|
||||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Util;
|
namespace ShlinkioTest\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
@ -12,9 +12,10 @@ use Prophecy\Argument;
|
|||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
use Symfony\Component\Lock;
|
use Symfony\Component\Lock;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@ -47,7 +48,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||||
{
|
{
|
||||||
$mustBeUpdated = fn () => self::assertTrue(true);
|
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||||
$prev = new RuntimeException('');
|
$prev = new DbUpdateException('');
|
||||||
|
|
||||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||||
$getMeta = $this->geoLiteDbReader->metadata();
|
$getMeta = $this->geoLiteDbReader->metadata();
|
||||||
@ -80,7 +81,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch(
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch(
|
||||||
Chronos::now()->subDays($days)->getTimestamp(),
|
Chronos::now()->subDays($days)->getTimestamp(),
|
||||||
));
|
));
|
||||||
$prev = new RuntimeException('');
|
$prev = new DbUpdateException('');
|
||||||
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -110,15 +111,16 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
* @test
|
* @test
|
||||||
* @dataProvider provideSmallDays
|
* @dataProvider provideSmallDays
|
||||||
*/
|
*/
|
||||||
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(string|int $buildEpoch): void
|
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
|
||||||
{
|
{
|
||||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch($buildEpoch));
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch($buildEpoch));
|
||||||
$download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void {
|
$download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void {
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->geolocationDbUpdater()->checkDbUpdate();
|
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||||
|
|
||||||
|
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
|
||||||
$fileExists->shouldHaveBeenCalledOnce();
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
$getMeta->shouldHaveBeenCalledOnce();
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
$download->shouldNotHaveBeenCalled();
|
$download->shouldNotHaveBeenCalled();
|
||||||
@ -174,8 +176,9 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
||||||
{
|
{
|
||||||
$this->geolocationDbUpdater($options)->checkDbUpdate();
|
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
||||||
|
|
||||||
|
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
|
||||||
$this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled();
|
$this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
$this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled();
|
$this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
|||||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ return [
|
|||||||
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
||||||
Visit\RequestTracker::class => ConfigAbstractFactory::class,
|
Visit\RequestTracker::class => ConfigAbstractFactory::class,
|
||||||
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
||||||
|
Visit\VisitToLocationHelper::class => ConfigAbstractFactory::class,
|
||||||
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
||||||
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
||||||
|
|
||||||
@ -108,6 +110,7 @@ return [
|
|||||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||||
],
|
],
|
||||||
Visit\VisitLocator::class => ['em'],
|
Visit\VisitLocator::class => ['em'],
|
||||||
|
Visit\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
|
||||||
Visit\VisitsStatsHelper::class => ['em'],
|
Visit\VisitsStatsHelper::class => ['em'],
|
||||||
Tag\TagService::class => ['em'],
|
Tag\TagService::class => ['em'],
|
||||||
Service\ShortUrl\DeleteShortUrlService::class => [
|
Service\ShortUrl\DeleteShortUrlService::class => [
|
||||||
|
@ -6,10 +6,12 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
|
||||||
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
|
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
|
||||||
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
||||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitLocator;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelper;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||||
|
|
||||||
@ -20,6 +22,9 @@ return [
|
|||||||
EventDispatcher\Event\UrlVisited::class => [
|
EventDispatcher\Event\UrlVisited::class => [
|
||||||
EventDispatcher\LocateVisit::class,
|
EventDispatcher\LocateVisit::class,
|
||||||
],
|
],
|
||||||
|
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||||
|
EventDispatcher\LocateUnlocatedVisits::class,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'async' => [
|
'async' => [
|
||||||
EventDispatcher\Event\VisitLocated::class => [
|
EventDispatcher\Event\VisitLocated::class => [
|
||||||
@ -40,6 +45,7 @@ return [
|
|||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||||
|
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||||
@ -69,6 +75,9 @@ return [
|
|||||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
],
|
],
|
||||||
|
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||||
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
|
],
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
],
|
],
|
||||||
@ -83,6 +92,7 @@ return [
|
|||||||
DbUpdater::class,
|
DbUpdater::class,
|
||||||
EventDispatcherInterface::class,
|
EventDispatcherInterface::class,
|
||||||
],
|
],
|
||||||
|
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||||
'httpClient',
|
'httpClient',
|
||||||
'em',
|
'em',
|
||||||
@ -132,7 +142,11 @@ return [
|
|||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
'config.redis.pub_sub_enabled',
|
'config.redis.pub_sub_enabled',
|
||||||
],
|
],
|
||||||
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||||
|
GeolocationDbUpdater::class,
|
||||||
|
'Logger_Shlink',
|
||||||
|
EventDispatcherInterface::class,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -19,8 +19,8 @@ use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
|||||||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ShortUrlResolverInterface $urlResolver,
|
private readonly ShortUrlResolverInterface $urlResolver,
|
||||||
private RequestTrackerInterface $requestTracker,
|
private readonly RequestTrackerInterface $requestTracker,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
||||||
|
|
||||||
|
final class GeoLiteDbCreated
|
||||||
|
{
|
||||||
|
}
|
40
module/Core/src/EventDispatcher/LocateUnlocatedVisits.php
Normal file
40
module/Core/src/EventDispatcher/LocateUnlocatedVisits.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
|
||||||
|
class LocateUnlocatedVisits implements VisitGeolocationHelperInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly VisitLocatorInterface $locator,
|
||||||
|
private readonly VisitToLocationHelperInterface $visitToLocation,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(GeoLiteDbCreated $event): void
|
||||||
|
{
|
||||||
|
$this->locator->locateUnlocatedVisits($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IpCannotBeLocatedException
|
||||||
|
*/
|
||||||
|
public function geolocateVisit(Visit $visit): Location
|
||||||
|
{
|
||||||
|
return $this->visitToLocation->resolveVisitLocation($visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -4,16 +4,22 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||||
|
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class UpdateGeoLiteDb
|
class UpdateGeoLiteDb
|
||||||
{
|
{
|
||||||
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater, private LoggerInterface $logger)
|
public function __construct(
|
||||||
{
|
private readonly GeolocationDbUpdaterInterface $dbUpdater,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(): void
|
public function __invoke(): void
|
||||||
@ -32,7 +38,10 @@ class UpdateGeoLiteDb
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->dbUpdater->checkDbUpdate($beforeDownload, $handleProgress);
|
$result = $this->dbUpdater->checkDbUpdate($beforeDownload, $handleProgress);
|
||||||
|
if ($result === GeolocationResult::DB_CREATED) {
|
||||||
|
$this->eventDispatcher->dispatch(new GeoLiteDbCreated());
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->logger->error('GeoLite2 database download failed. {e}', ['e' => $e]);
|
$this->logger->error('GeoLite2 database download failed. {e}', ['e' => $e]);
|
||||||
}
|
}
|
||||||
|
@ -4,35 +4,40 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Exception;
|
namespace Shlinkio\Shlink\Core\Exception;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class IpCannotBeLocatedException extends RuntimeException
|
class IpCannotBeLocatedException extends RuntimeException
|
||||||
{
|
{
|
||||||
private bool $isNonLocatableAddress = true;
|
private function __construct(
|
||||||
|
string $message,
|
||||||
|
public readonly UnlocatableIpType $type,
|
||||||
|
int $code = 0,
|
||||||
|
?Throwable $previous = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
public static function forEmptyAddress(): self
|
public static function forEmptyAddress(): self
|
||||||
{
|
{
|
||||||
return new self('Ignored visit with no IP address');
|
return new self('Ignored visit with no IP address', UnlocatableIpType::EMPTY_ADDRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function forLocalhost(): self
|
public static function forLocalhost(): self
|
||||||
{
|
{
|
||||||
return new self('Ignored localhost address');
|
return new self('Ignored localhost address', UnlocatableIpType::LOCALHOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function forError(Throwable $e): self
|
public static function forError(Throwable $e): self
|
||||||
{
|
{
|
||||||
$e = new self('An error occurred while locating IP', $e->getCode(), $e);
|
return new self('An error occurred while locating IP', UnlocatableIpType::ERROR, $e->getCode(), $e);
|
||||||
$e->isNonLocatableAddress = false;
|
|
||||||
|
|
||||||
return $e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if this error belongs to an address that will never be possible locate
|
* Tells if this belongs to an address that will never be possible to locate
|
||||||
*/
|
*/
|
||||||
public function isNonLocatableAddress(): bool
|
public function isNonLocatableAddress(): bool
|
||||||
{
|
{
|
||||||
return $this->isNonLocatableAddress;
|
return $this->type !== UnlocatableIpType::ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
module/Core/src/Visit/Model/UnlocatableIpType.php
Normal file
10
module/Core/src/Visit/Model/UnlocatableIpType.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Visit\Model;
|
||||||
|
|
||||||
|
enum UnlocatableIpType
|
||||||
|
{
|
||||||
|
case EMPTY_ADDRESS;
|
||||||
|
case LOCALHOST;
|
||||||
|
case ERROR;
|
||||||
|
}
|
40
module/Core/src/Visit/VisitToLocationHelper.php
Normal file
40
module/Core/src/Visit/VisitToLocationHelper.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||||
|
|
||||||
|
class VisitToLocationHelper implements VisitToLocationHelperInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly IpLocationResolverInterface $ipLocationResolver)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IpCannotBeLocatedException
|
||||||
|
*/
|
||||||
|
public function resolveVisitLocation(Visit $visit): Location
|
||||||
|
{
|
||||||
|
if (! $visit->hasRemoteAddr()) {
|
||||||
|
throw IpCannotBeLocatedException::forEmptyAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddr = $visit->getRemoteAddr() ?? '';
|
||||||
|
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||||
|
throw IpCannotBeLocatedException::forLocalhost();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||||
|
} catch (WrongIpException $e) {
|
||||||
|
throw IpCannotBeLocatedException::forError($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
module/Core/src/Visit/VisitToLocationHelperInterface.php
Normal file
17
module/Core/src/Visit/VisitToLocationHelperInterface.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
|
||||||
|
interface VisitToLocationHelperInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws IpCannotBeLocatedException
|
||||||
|
*/
|
||||||
|
public function resolveVisitLocation(Visit $visit): Location;
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\LocateUnlocatedVisits;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
|
||||||
|
class LocateUnlocatedVisitsTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private LocateUnlocatedVisits $listener;
|
||||||
|
private ObjectProphecy $locator;
|
||||||
|
private ObjectProphecy $visitToLocation;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->locator = $this->prophesize(VisitLocatorInterface::class);
|
||||||
|
$this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class);
|
||||||
|
|
||||||
|
$this->listener = new LocateUnlocatedVisits($this->locator->reveal(), $this->visitToLocation->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function locatorIsCalledWhenInvoked(): void
|
||||||
|
{
|
||||||
|
($this->listener)(new GeoLiteDbCreated());
|
||||||
|
$this->locator->locateUnlocatedVisits($this->listener)->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function visitToLocationHelperIsCalledToGeolocateVisits(): void
|
||||||
|
{
|
||||||
|
$visit = Visit::forBasePath(Visitor::emptyInstance());
|
||||||
|
$location = Location::emptyInstance();
|
||||||
|
|
||||||
|
$resolve = $this->visitToLocation->resolveVisitLocation($visit)->willReturn($location);
|
||||||
|
|
||||||
|
$result = $this->listener->geolocateVisit($visit);
|
||||||
|
|
||||||
|
self::assertSame($location, $result);
|
||||||
|
$resolve->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
@ -8,11 +8,16 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
|
||||||
use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
|
use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
|
||||||
|
|
||||||
|
use function Functional\map;
|
||||||
|
|
||||||
class UpdateGeoLiteDbTest extends TestCase
|
class UpdateGeoLiteDbTest extends TestCase
|
||||||
{
|
{
|
||||||
use ProphecyTrait;
|
use ProphecyTrait;
|
||||||
@ -20,13 +25,19 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
private UpdateGeoLiteDb $listener;
|
private UpdateGeoLiteDb $listener;
|
||||||
private ObjectProphecy $dbUpdater;
|
private ObjectProphecy $dbUpdater;
|
||||||
private ObjectProphecy $logger;
|
private ObjectProphecy $logger;
|
||||||
|
private ObjectProphecy $eventDispatcher;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||||
|
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
|
||||||
|
|
||||||
$this->listener = new UpdateGeoLiteDb($this->dbUpdater->reveal(), $this->logger->reveal());
|
$this->listener = new UpdateGeoLiteDb(
|
||||||
|
$this->dbUpdater->reveal(),
|
||||||
|
$this->logger->reveal(),
|
||||||
|
$this->eventDispatcher->reveal(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@ -42,6 +53,7 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||||
$logError->shouldHaveBeenCalledOnce();
|
$logError->shouldHaveBeenCalledOnce();
|
||||||
$this->logger->notice(Argument::cetera())->shouldNotHaveBeenCalled();
|
$this->logger->notice(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,9 +63,11 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void
|
public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void
|
||||||
{
|
{
|
||||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||||
function (array $args) use ($oldDbExists): void {
|
function (array $args) use ($oldDbExists): GeolocationResult {
|
||||||
[$firstCallback] = $args;
|
[$firstCallback] = $args;
|
||||||
$firstCallback($oldDbExists);
|
$firstCallback($oldDbExists);
|
||||||
|
|
||||||
|
return GeolocationResult::DB_IS_UP_TO_DATE;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
$logNotice = $this->logger->notice($expectedMessage);
|
$logNotice = $this->logger->notice($expectedMessage);
|
||||||
@ -63,6 +77,7 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||||
$logNotice->shouldHaveBeenCalledOnce();
|
$logNotice->shouldHaveBeenCalledOnce();
|
||||||
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFlags(): iterable
|
public function provideFlags(): iterable
|
||||||
@ -82,13 +97,15 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
?string $expectedMessage,
|
?string $expectedMessage,
|
||||||
): void {
|
): void {
|
||||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||||
function (array $args) use ($total, $downloaded, $oldDbExists): void {
|
function (array $args) use ($total, $downloaded, $oldDbExists): GeolocationResult {
|
||||||
[, $secondCallback] = $args;
|
[, $secondCallback] = $args;
|
||||||
|
|
||||||
// Invoke several times to ensure the log is printed only once
|
// Invoke several times to ensure the log is printed only once
|
||||||
$secondCallback($total, $downloaded, $oldDbExists);
|
$secondCallback($total, $downloaded, $oldDbExists);
|
||||||
$secondCallback($total, $downloaded, $oldDbExists);
|
$secondCallback($total, $downloaded, $oldDbExists);
|
||||||
$secondCallback($total, $downloaded, $oldDbExists);
|
$secondCallback($total, $downloaded, $oldDbExists);
|
||||||
|
|
||||||
|
return GeolocationResult::DB_UPDATED;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
$logNotice = $this->logger->notice($expectedMessage ?? Argument::cetera());
|
$logNotice = $this->logger->notice($expectedMessage ?? Argument::cetera());
|
||||||
@ -102,6 +119,7 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
}
|
}
|
||||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||||
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
$this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideDownloaded(): iterable
|
public function provideDownloaded(): iterable
|
||||||
@ -115,4 +133,28 @@ class UpdateGeoLiteDbTest extends TestCase
|
|||||||
yield [100, 101, true, 'Finished updating GeoLite2 db file'];
|
yield [100, 101, true, 'Finished updating GeoLite2 db file'];
|
||||||
yield [100, 101, false, 'Finished downloading GeoLite2 db file'];
|
yield [100, 101, false, 'Finished downloading GeoLite2 db file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideGeolocationResults
|
||||||
|
*/
|
||||||
|
public function dispatchesEventOnlyWhenDbFileHasBeenCreatedForTheFirstTime(
|
||||||
|
GeolocationResult $result,
|
||||||
|
int $expectedDispatches,
|
||||||
|
): void {
|
||||||
|
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willReturn($result);
|
||||||
|
|
||||||
|
($this->listener)();
|
||||||
|
|
||||||
|
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||||
|
$this->eventDispatcher->dispatch(new GeoLiteDbCreated())->shouldHaveBeenCalledTimes($expectedDispatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideGeolocationResults(): iterable
|
||||||
|
{
|
||||||
|
return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [
|
||||||
|
$value,
|
||||||
|
$value === GeolocationResult::DB_CREATED ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ use LogicException;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class IpCannotBeLocatedExceptionTest extends TestCase
|
class IpCannotBeLocatedExceptionTest extends TestCase
|
||||||
@ -22,6 +23,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase
|
|||||||
self::assertEquals('Ignored visit with no IP address', $e->getMessage());
|
self::assertEquals('Ignored visit with no IP address', $e->getMessage());
|
||||||
self::assertEquals(0, $e->getCode());
|
self::assertEquals(0, $e->getCode());
|
||||||
self::assertNull($e->getPrevious());
|
self::assertNull($e->getPrevious());
|
||||||
|
self::assertEquals(UnlocatableIpType::EMPTY_ADDRESS, $e->type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@ -33,6 +35,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase
|
|||||||
self::assertEquals('Ignored localhost address', $e->getMessage());
|
self::assertEquals('Ignored localhost address', $e->getMessage());
|
||||||
self::assertEquals(0, $e->getCode());
|
self::assertEquals(0, $e->getCode());
|
||||||
self::assertNull($e->getPrevious());
|
self::assertNull($e->getPrevious());
|
||||||
|
self::assertEquals(UnlocatableIpType::LOCALHOST, $e->type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,6 +50,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase
|
|||||||
self::assertEquals('An error occurred while locating IP', $e->getMessage());
|
self::assertEquals('An error occurred while locating IP', $e->getMessage());
|
||||||
self::assertEquals($prev->getCode(), $e->getCode());
|
self::assertEquals($prev->getCode(), $e->getCode());
|
||||||
self::assertSame($prev, $e->getPrevious());
|
self::assertSame($prev, $e->getPrevious());
|
||||||
|
self::assertEquals(UnlocatableIpType::ERROR, $e->type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideErrors(): iterable
|
public function provideErrors(): iterable
|
||||||
|
@ -129,7 +129,7 @@ class VisitLocatorTest extends TestCase
|
|||||||
public function geolocateVisit(Visit $visit): Location
|
public function geolocateVisit(Visit $visit): Location
|
||||||
{
|
{
|
||||||
throw $this->isNonLocatableAddress
|
throw $this->isNonLocatableAddress
|
||||||
? new IpCannotBeLocatedException('Cannot be located')
|
? IpCannotBeLocatedException::forEmptyAddress()
|
||||||
: IpCannotBeLocatedException::forError(new Exception(''));
|
: IpCannotBeLocatedException::forError(new Exception(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
66
module/Core/test/Visit/VisitToLocationHelperTest.php
Normal file
66
module/Core/test/Visit/VisitToLocationHelperTest.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Visit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelper;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||||
|
|
||||||
|
class VisitToLocationHelperTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private VisitToLocationHelper $helper;
|
||||||
|
private ObjectProphecy $ipLocationResolver;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||||
|
$this->helper = new VisitToLocationHelper($this->ipLocationResolver->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideNonLocatableVisits
|
||||||
|
*/
|
||||||
|
public function throwsExpectedErrorForNonLocatableVisit(
|
||||||
|
Visit $visit,
|
||||||
|
IpCannotBeLocatedException $expectedException,
|
||||||
|
): void {
|
||||||
|
$this->expectExceptionObject($expectedException);
|
||||||
|
$this->ipLocationResolver->resolveIpLocation(Argument::cetera())->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->helper->resolveVisitLocation($visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideNonLocatableVisits(): iterable
|
||||||
|
{
|
||||||
|
yield [Visit::forBasePath(Visitor::emptyInstance()), IpCannotBeLocatedException::forEmptyAddress()];
|
||||||
|
yield [
|
||||||
|
Visit::forBasePath(new Visitor('foo', 'bar', IpAddress::LOCALHOST, '')),
|
||||||
|
IpCannotBeLocatedException::forLocalhost(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function throwsGenericErrorWhenResolvingIpFails(): void
|
||||||
|
{
|
||||||
|
$e = new WrongIpException('');
|
||||||
|
|
||||||
|
$this->expectExceptionObject(IpCannotBeLocatedException::forError($e));
|
||||||
|
$this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow($e)
|
||||||
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$this->helper->resolveVisitLocation(Visit::forBasePath(new Visitor('foo', 'bar', '1.2.3.4', '')));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user