mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-17 15:59:56 +03:00
Merge pull request #258 from acelaya/feature/geolocation
Feature/geolocation
This commit is contained in:
commit
d4758b0e91
75 changed files with 1095 additions and 322 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,5 +5,6 @@ composer.phar
|
|||
vendor/
|
||||
.env
|
||||
data/database.sqlite
|
||||
data/GeoLite2-City.mmdb
|
||||
docs/swagger-ui
|
||||
docker-compose.override.yml
|
||||
|
|
|
@ -14,6 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
This new option will be asked by the installer both for new shlink installations and for any previous shlink version which is updated.
|
||||
|
||||
* [#189](https://github.com/shlinkio/shlink/issues/189) and [#240](https://github.com/shlinkio/shlink/issues/240) Added new [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/)-based geolocation service which is faster and more reliable than previous one.
|
||||
|
||||
It does not have API limit problems, since it uses a local database file.
|
||||
|
||||
Previous service is still used as a fallback in case GeoLite DB does not contain any IP address.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#241](https://github.com/shlinkio/shlink/issues/241) Fixed columns in `visit_locations` table, to be snake_case instead of camelCase.
|
||||
|
|
|
@ -104,6 +104,12 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
|
|||
|
||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||
|
||||
* Update IP geolocation database: `/path/to/shlink/bin/cli visit:update-db`
|
||||
|
||||
When shlink is installed it downloads a fresh [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) db file. Running this command will update this file.
|
||||
|
||||
The file is updated the first Tuesday of every month, so it should be enough running this command the first Wednesday.
|
||||
|
||||
* Generate website previews: `/path/to/shlink/bin/cli short-url:process-previews`
|
||||
|
||||
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
|
||||
|
@ -186,3 +192,5 @@ Available commands:
|
|||
visit
|
||||
visit:process Processes visits where location is not set yet
|
||||
```
|
||||
|
||||
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"doctrine/orm": "^2.5",
|
||||
"endroid/qr-code": "^1.7",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"geoip2/geoip2": "^2.9",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"lstrojny/functional-php": "^1.8",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
|
@ -47,6 +48,7 @@
|
|||
"zendframework/zend-stdlib": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"filp/whoops": "^2.0",
|
||||
"infection/infection": "^0.11.0",
|
||||
"phpstan/phpstan": "^0.10.0",
|
||||
|
|
|
@ -23,6 +23,12 @@ return [
|
|||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
|
||||
'lazy_services' => [
|
||||
'proxies_target_dir' => 'data/proxies',
|
||||
'proxies_namespace' => 'ShlinkProxy',
|
||||
'write_proxy_files' => true,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
12
config/autoload/dependencies.local.php.dist
Normal file
12
config/autoload/dependencies.local.php.dist
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'lazy_services' => [
|
||||
'write_proxy_files' => false,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
12
config/autoload/geolite2.global.php
Normal file
12
config/autoload/geolite2.global.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
|
||||
],
|
||||
|
||||
];
|
|
@ -18,6 +18,7 @@ return [
|
|||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||
|
|
|
@ -3,7 +3,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
|
@ -25,6 +26,7 @@ return [
|
|||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
||||
|
@ -65,9 +67,10 @@ return [
|
|||
|
||||
Command\Visit\ProcessVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpApiLocationResolver::class,
|
||||
IpLocationResolverInterface::class,
|
||||
'translator',
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class, 'translator'],
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||
Command\Config\GenerateSecretCommand::class => ['translator'],
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-09-16 18:36+0200\n"
|
||||
"PO-Revision-Date: 2018-09-16 18:37+0200\n"
|
||||
"POT-Creation-Date: 2018-11-12 21:01+0100\n"
|
||||
"PO-Revision-Date: 2018-11-12 21:03+0100\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
|
@ -340,6 +340,9 @@ msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
|
|||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
msgid "Ignored visit with no IP address"
|
||||
msgstr "Ignorada visita sin dirección IP"
|
||||
|
||||
msgid "Processing IP"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
|
@ -350,16 +353,33 @@ msgstr "Ignorada IP de localhost"
|
|||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "An error occurred while locating IP"
|
||||
msgstr "Se produjo un error al localizar la IP"
|
||||
|
||||
#, php-format
|
||||
msgid "IP location resolver limit reached. Waiting %s seconds..."
|
||||
msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
||||
msgid "An error occurred while locating IP. Skipped"
|
||||
msgstr "Se produjo un error al localizar la IP. Ignorado"
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
||||
msgid "Updates the GeoLite2 database file used to geolocate IP addresses"
|
||||
msgstr ""
|
||||
"Actualiza el fichero de base de datos de GeoLite2 usado para geolocalizar "
|
||||
"direcciones IP"
|
||||
|
||||
msgid ""
|
||||
"The GeoLite2 database is updated first Tuesday every month, so this command "
|
||||
"should be ideally run every first Wednesday"
|
||||
msgstr ""
|
||||
"La base de datos de GeoLite2 se actualiza el primer Martes de cada mes, por "
|
||||
"lo que la opción ideal es ejecutar este comando cada primer miércoles de mes"
|
||||
|
||||
msgid "GeoLite2 database properly updated"
|
||||
msgstr "Base de datos de GeoLite2 correctamente actualizada"
|
||||
|
||||
msgid "An error occurred while updating GeoLite2 database"
|
||||
msgstr "Se produjo un error al actualizar la base de datos de GeoLite2"
|
||||
|
||||
#~ msgid "IP location resolver limit reached. Waiting %s seconds..."
|
||||
#~ msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
||||
|
||||
#~ msgid "Remote Address"
|
||||
#~ msgstr "Dirección remota"
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
|
@ -13,7 +13,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sleep;
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
|
@ -41,7 +40,7 @@ class ProcessVisitsCommand extends Command
|
|||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
@ -57,7 +56,6 @@ class ProcessVisitsCommand extends Command
|
|||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
$count = 0;
|
||||
foreach ($visits as $visit) {
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$io->writeln(
|
||||
|
@ -68,15 +66,14 @@ class ProcessVisitsCommand extends Command
|
|||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
$io->write(sprintf('%s <fg=blue>%s</>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$io->writeln(
|
||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
sprintf(' [<comment>%s</comment>]', $this->translator->translate('Ignored localhost address'))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$count++;
|
||||
try {
|
||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
|
||||
|
@ -85,27 +82,20 @@ class ProcessVisitsCommand extends Command
|
|||
$this->visitService->saveVisit($visit);
|
||||
|
||||
$io->writeln(sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
' [<info>' . $this->translator->translate('Address located at "%s"') . '</info>]',
|
||||
$location->getCountryName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
$io->writeln(
|
||||
sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||
sprintf(
|
||||
' [<fg=red>%s</>]',
|
||||
$this->translator->translate('An error occurred while locating IP. Skipped')
|
||||
)
|
||||
);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
}
|
||||
|
||||
if ($count === $this->ipLocationResolver->getApiLimit()) {
|
||||
$count = 0;
|
||||
$seconds = $this->ipLocationResolver->getApiInterval();
|
||||
$io->note(sprintf(
|
||||
$this->translator->translate('IP location resolver limit reached. Waiting %s seconds...'),
|
||||
$seconds
|
||||
));
|
||||
sleep($seconds);
|
||||
}
|
||||
}
|
||||
|
||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||
|
|
74
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
74
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
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 Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class UpdateDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:update-db';
|
||||
|
||||
/**
|
||||
* @var DbUpdaterInterface
|
||||
*/
|
||||
private $geoLiteDbUpdater;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DbUpdaterInterface $geoLiteDbUpdater, TranslatorInterface $translator)
|
||||
{
|
||||
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Updates the GeoLite2 database file used to geolocate IP addresses')
|
||||
)
|
||||
->setHelp($this->translator->translate(
|
||||
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
||||
. 'every first Wednesday'
|
||||
));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$progressBar = new ProgressBar($output);
|
||||
$progressBar->start();
|
||||
|
||||
try {
|
||||
$this->geoLiteDbUpdater->downloadFreshCopy(function (int $total, int $downloaded) use ($progressBar) {
|
||||
$progressBar->setMaxSteps($total);
|
||||
$progressBar->setProgress($downloaded);
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->success($this->translator->translate('GeoLite2 database properly updated'));
|
||||
} catch (RuntimeException $e) {
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->error($this->translator->translate('An error occurred while updating GeoLite2 database'));
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
public function providedApiKeyIsDisabled()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
|
@ -52,7 +52,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
|
|
|
@ -39,7 +39,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||
*/
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
||||
$this->apiKeyService->create(null)->shouldBeCalledOnce()
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
|
@ -51,7 +51,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||
*/
|
||||
public function expirationDateIsDefinedIfProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledTimes(1)
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
|
|
|
@ -41,7 +41,7 @@ class ListKeysCommandTest extends TestCase
|
|||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
])->shouldBeCalledOnce();
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
]);
|
||||
|
@ -55,7 +55,7 @@ class ListKeysCommandTest extends TestCase
|
|||
$this->apiKeyService->listKeys(true)->willReturn([
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
])->shouldBeCalledOnce();
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
'--enabledOnly' => true,
|
||||
|
|
|
@ -50,7 +50,7 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,7 +67,7 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,6 +117,6 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||
$shortCode
|
||||
), $output);
|
||||
$this->assertContains('Short URL was not deleted.', $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,11 +60,11 @@ class GeneratePreviewCommandTest extends TestCase
|
|||
new ShortUrl('https://bar.com'),
|
||||
new ShortUrl('http://baz.com/something'),
|
||||
]);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||
|
||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:process-previews',
|
||||
|
@ -82,7 +82,7 @@ class GeneratePreviewCommandTest extends TestCase
|
|||
new ShortUrl('http://baz.com/something'),
|
||||
];
|
||||
$paginator = $this->createPaginator($items);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
||||
->shouldBeCalledTimes(count($items));
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class GenerateShortcodeCommandTest extends TestCase
|
|||
->willReturn(
|
||||
(new ShortUrl(''))->setShortCode('abc123')
|
||||
)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
|
@ -63,7 +63,7 @@ class GenerateShortcodeCommandTest extends TestCase
|
|||
public function exceptionWhileParsingLongUrlOutputsError()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
|
|
|
@ -46,7 +46,7 @@ class GetVisitsCommandTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
|
@ -64,7 +64,7 @@ class GetVisitsCommandTest extends TestCase
|
|||
$endDate = '2016-02-01';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
|
@ -84,7 +84,7 @@ class GetVisitsCommandTest extends TestCase
|
|||
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->setVisitLocation(
|
||||
new VisitLocation(['country_name' => 'Spain'])
|
||||
),
|
||||
])->shouldBeCalledTimes(1);
|
||||
])->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
|
|
|
@ -41,7 +41,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||
public function noInputCallsListJustOnce()
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
|
@ -78,7 +78,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||
}
|
||||
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
|
@ -91,7 +91,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||
{
|
||||
$page = 5;
|
||||
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
|
@ -106,7 +106,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
|
|
|
@ -45,7 +45,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
$expectedUrl = 'http://domain.com/foo/bar';
|
||||
$shortUrl = new ShortUrl($expectedUrl);
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
|
@ -62,7 +62,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
|
@ -79,7 +79,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
|
|
|
@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase;
|
|||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
|
@ -17,28 +17,26 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function count;
|
||||
use function round;
|
||||
|
||||
class ProcessVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $visitService;
|
||||
private $visitService;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $ipResolver;
|
||||
private $ipResolver;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitService::class);
|
||||
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||
$this->ipResolver->getApiLimit()->willReturn(10000000000);
|
||||
|
||||
$command = new ProcessVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
|
@ -64,7 +62,7 @@ class ProcessVisitsCommandTest extends TestCase
|
|||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
];
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
|
@ -96,7 +94,7 @@ class ProcessVisitsCommandTest extends TestCase
|
|||
new Visit($shortUrl, new Visitor('', '', null)),
|
||||
];
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 4);
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
|
@ -109,43 +107,4 @@ class ProcessVisitsCommandTest extends TestCase
|
|||
$this->assertContains('Ignored localhost address', $output);
|
||||
$this->assertContains('Ignored visit with no IP address', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function sleepsEveryTimeTheApiLimitIsReached()
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
|
||||
$visits = [
|
||||
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
];
|
||||
$apiLimit = 3;
|
||||
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits);
|
||||
$this->visitService->saveVisit(Argument::any())->will(function () {
|
||||
});
|
||||
|
||||
$getApiLimit = $this->ipResolver->getApiLimit()->willReturn($apiLimit);
|
||||
$getApiInterval = $this->ipResolver->getApiInterval()->willReturn(0);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
]);
|
||||
|
||||
$getApiLimit->shouldHaveBeenCalledTimes(count($visits));
|
||||
$getApiInterval->shouldHaveBeenCalledTimes(round(count($visits) / $apiLimit));
|
||||
$resolveIpLocation->shouldHaveBeenCalledTimes(count($visits));
|
||||
}
|
||||
}
|
||||
|
|
66
module/CLI/test/Command/Visit/UpdateDbCommandTest.php
Normal file
66
module/CLI/test/Command/Visit/UpdateDbCommandTest.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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\UpdateDbCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class UpdateDbCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $dbUpdater;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
|
||||
$command = new UpdateDbCommand($this->dbUpdater->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function successMessageIsPrintedIfEverythingWorks()
|
||||
{
|
||||
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
|
||||
});
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('GeoLite2 database properly updated', $output);
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorMessageIsPrintedIfAnExceptionIsThrown()
|
||||
{
|
||||
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('An error occurred while updating GeoLite2 database', $output);
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common;
|
|||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GeoIp2\Database\Reader;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -13,6 +14,7 @@ use Symfony\Component\Filesystem\Filesystem;
|
|||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\Proxy\LazyServiceFactory;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -23,6 +25,7 @@ return [
|
|||
Cache::class => Factory\CacheFactory::class,
|
||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
Reader::class => ConfigAbstractFactory::class,
|
||||
|
||||
Translator::class => Factory\TranslatorFactory::class,
|
||||
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
|
||||
|
@ -32,26 +35,64 @@ return [
|
|||
|
||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||
|
||||
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||
IpGeolocation\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||
IpGeolocation\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
|
||||
IpGeolocation\EmptyIpLocationResolver::class => InvokableFactory::class,
|
||||
IpGeolocation\ChainIpLocationResolver::class => ConfigAbstractFactory::class,
|
||||
IpGeolocation\GeoLite2\GeoLite2Options::class => ConfigAbstractFactory::class,
|
||||
IpGeolocation\GeoLite2\DbUpdater::class => ConfigAbstractFactory::class,
|
||||
|
||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleClient::class,
|
||||
'translator' => Translator::class,
|
||||
|
||||
'logger' => LoggerInterface::class,
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
|
||||
IpGeolocation\IpLocationResolverInterface::class => IpGeolocation\ChainIpLocationResolver::class,
|
||||
],
|
||||
'abstract_factories' => [
|
||||
Factory\DottedAccessConfigAbstractFactory::class,
|
||||
],
|
||||
'delegators' => [
|
||||
// The GeoLite2 db reader has to be lazy so that it does not try to load the DB file at app bootstrapping.
|
||||
// By doing so, it would fail the first time shlink tries to download it.
|
||||
Reader::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'lazy_services' => [
|
||||
'class_map' => [
|
||||
Reader::class => Reader::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Reader::class => ['config.geolite2.db_location'],
|
||||
|
||||
Template\Extension\TranslatorExtension::class => ['translator'],
|
||||
Middleware\LocaleMiddleware::class => ['translator'],
|
||||
Service\IpApiLocationResolver::class => ['httpClient'],
|
||||
|
||||
IpGeolocation\IpApiLocationResolver::class => ['httpClient'],
|
||||
IpGeolocation\GeoLite2LocationResolver::class => [Reader::class],
|
||||
IpGeolocation\ChainIpLocationResolver::class => [
|
||||
IpGeolocation\GeoLite2LocationResolver::class,
|
||||
IpGeolocation\IpApiLocationResolver::class,
|
||||
IpGeolocation\EmptyIpLocationResolver::class,
|
||||
],
|
||||
IpGeolocation\GeoLite2\GeoLite2Options::class => ['config.geolite2'],
|
||||
IpGeolocation\GeoLite2\DbUpdater::class => [
|
||||
GuzzleClient::class,
|
||||
Filesystem::class,
|
||||
IpGeolocation\GeoLite2\GeoLite2Options::class,
|
||||
],
|
||||
|
||||
Service\PreviewGenerator::class => [
|
||||
Image\ImageBuilder::class,
|
||||
Filesystem::class,
|
||||
|
|
38
module/Common/src/IpGeolocation/ChainIpLocationResolver.php
Normal file
38
module/Common/src/IpGeolocation/ChainIpLocationResolver.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class ChainIpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @var IpLocationResolverInterface[]
|
||||
*/
|
||||
private $resolvers;
|
||||
|
||||
public function __construct(IpLocationResolverInterface ...$resolvers)
|
||||
{
|
||||
$this->resolvers = $resolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
$error = null;
|
||||
|
||||
foreach ($this->resolvers as $resolver) {
|
||||
try {
|
||||
return $resolver->resolveIpLocation($ipAddress);
|
||||
} catch (WrongIpException $e) {
|
||||
$error = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// If this instruction is reached, it means no resolver was capable of resolving the address
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $error);
|
||||
}
|
||||
}
|
25
module/Common/src/IpGeolocation/EmptyIpLocationResolver.php
Normal file
25
module/Common/src/IpGeolocation/EmptyIpLocationResolver.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class EmptyIpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
return [
|
||||
'country_code' => '',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'time_zone' => '',
|
||||
];
|
||||
}
|
||||
}
|
106
module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php
Normal file
106
module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use PharData;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Throwable;
|
||||
use function sprintf;
|
||||
|
||||
class DbUpdater implements DbUpdaterInterface
|
||||
{
|
||||
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
|
||||
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
/**
|
||||
* @var GeoLite2Options
|
||||
*/
|
||||
private $options;
|
||||
|
||||
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(callable $handleProgress = null): void
|
||||
{
|
||||
$tempDir = $this->options->getTempDir();
|
||||
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
||||
|
||||
$this->downloadDbFile($compressedFile, $handleProgress);
|
||||
$tempFullPath = $this->extractDbFile($compressedFile, $tempDir);
|
||||
$this->copyNewDbFile($tempFullPath);
|
||||
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
||||
}
|
||||
|
||||
private function downloadDbFile(string $dest, callable $handleProgress = null): void
|
||||
{
|
||||
try {
|
||||
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
|
||||
RequestOptions::SINK => $dest,
|
||||
RequestOptions::PROGRESS => $handleProgress,
|
||||
]);
|
||||
} catch (Throwable | GuzzleException $e) {
|
||||
throw new RuntimeException(
|
||||
'An error occurred while trying to download a fresh copy of the GeoLite2 database',
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractDbFile(string $compressedFile, string $tempDir): string
|
||||
{
|
||||
try {
|
||||
$phar = new PharData($compressedFile);
|
||||
$internalPathToDb = sprintf('%s/%s', $phar->getBasename(), self::DB_DECOMPRESSED_FILE);
|
||||
$phar->extractTo($tempDir, $internalPathToDb, true);
|
||||
|
||||
return sprintf('%s/%s', $tempDir, $internalPathToDb);
|
||||
} catch (Throwable $e) {
|
||||
throw new RuntimeException(
|
||||
sprintf('An error occurred while trying to extract the GeoLite2 database from %s', $compressedFile),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyNewDbFile(string $from): void
|
||||
{
|
||||
try {
|
||||
$this->filesystem->copy($from, $this->options->getDbLocation(), true);
|
||||
} catch (FilesystemException\FileNotFoundException | FilesystemException\IOException $e) {
|
||||
throw new RuntimeException('An error occurred while trying to copy GeoLite2 db file to destination', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteTempFiles(array $files): void
|
||||
{
|
||||
try {
|
||||
$this->filesystem->remove($files);
|
||||
} catch (FilesystemException\IOException $e) {
|
||||
// Ignore any error produced when trying to delete temp files
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
|
||||
interface DbUpdaterInterface
|
||||
{
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(callable $handleProgress = null): void;
|
||||
}
|
46
module/Common/src/IpGeolocation/GeoLite2/GeoLite2Options.php
Normal file
46
module/Common/src/IpGeolocation/GeoLite2/GeoLite2Options.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||
|
||||
use Zend\Stdlib\AbstractOptions;
|
||||
|
||||
class GeoLite2Options extends AbstractOptions
|
||||
{
|
||||
private $dbLocation = '';
|
||||
private $tempDir = '';
|
||||
private $downloadFrom = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz';
|
||||
|
||||
public function getDbLocation(): string
|
||||
{
|
||||
return $this->dbLocation;
|
||||
}
|
||||
|
||||
protected function setDbLocation(string $dbLocation): self
|
||||
{
|
||||
$this->dbLocation = $dbLocation;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTempDir(): string
|
||||
{
|
||||
return $this->tempDir;
|
||||
}
|
||||
|
||||
protected function setTempDir(string $tempDir): self
|
||||
{
|
||||
$this->tempDir = $tempDir;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDownloadFrom(): string
|
||||
{
|
||||
return $this->downloadFrom;
|
||||
}
|
||||
|
||||
protected function setDownloadFrom(string $downloadFrom): self
|
||||
{
|
||||
$this->downloadFrom = $downloadFrom;
|
||||
return $this;
|
||||
}
|
||||
}
|
56
module/Common/src/IpGeolocation/GeoLite2LocationResolver.php
Normal file
56
module/Common/src/IpGeolocation/GeoLite2LocationResolver.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use GeoIp2\Exception\AddressNotFoundException;
|
||||
use GeoIp2\Model\City;
|
||||
use GeoIp2\Record\Subdivision;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use function Functional\first;
|
||||
|
||||
class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @var Reader
|
||||
*/
|
||||
private $geoLiteDbReader;
|
||||
|
||||
public function __construct(Reader $geoLiteDbReader)
|
||||
{
|
||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
try {
|
||||
$city = $this->geoLiteDbReader->city($ipAddress);
|
||||
return $this->mapFields($city);
|
||||
} catch (AddressNotFoundException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
} catch (InvalidDatabaseException $e) {
|
||||
throw new WrongIpException('Provided GeoLite2 db file is invalid', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(City $city): array
|
||||
{
|
||||
/** @var Subdivision $region */
|
||||
$region = first($city->subdivisions);
|
||||
|
||||
return [
|
||||
'country_code' => $city->country->isoCode ?? '',
|
||||
'country_name' => $city->country->name ?? '',
|
||||
'region_name' => $region->name ?? '',
|
||||
'city' => $city->city->name ?? '',
|
||||
'latitude' => $city->location->latitude ?? '',
|
||||
'longitude' => $city->location->longitude ?? '',
|
||||
'time_zone' => $city->location->timeZone ?? '',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
|
@ -25,8 +25,6 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $ipAddress
|
||||
* @return array
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
|
@ -53,24 +51,4 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
|||
'time_zone' => $entry['timezone'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interval in seconds that needs to be waited when the API limit is reached
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getApiInterval(): int
|
||||
{
|
||||
return 65; // ip-api interval is 1 minute. Return 5 extra seconds just in case
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getApiLimit(): ?int
|
||||
{
|
||||
return 145; // ip-api limit is 150 requests per minute. Leave 5 less requests just in case
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
interface IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
interface IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param string $ipAddress
|
||||
* @return array
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array;
|
||||
|
||||
/**
|
||||
* Returns the interval in seconds that needs to be waited when the API limit is reached
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getApiInterval(): int;
|
||||
|
||||
/**
|
||||
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getApiLimit(): ?int;
|
||||
}
|
1
module/Common/test-resources/.gitignore
vendored
Normal file
1
module/Common/test-resources/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
geolite2-testing-db
|
BIN
module/Common/test-resources/GeoLite2-City.tar.gz
Normal file
BIN
module/Common/test-resources/GeoLite2-City.tar.gz
Normal file
Binary file not shown.
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\IpGeolocation;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\ChainIpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
|
||||
class ChainIpLocationResolverTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ChainIpLocationResolver
|
||||
*/
|
||||
private $resolver;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $firstInnerResolver;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $secondInnerResolver;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->firstInnerResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->secondInnerResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
|
||||
$this->resolver = new ChainIpLocationResolver(
|
||||
$this->firstInnerResolver->reveal(),
|
||||
$this->secondInnerResolver->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function throwsExceptionWhenNoInnerResolverCanHandleTheResolution()
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
|
||||
$this->expectException(WrongIpException::class);
|
||||
$firstResolve->shouldBeCalledOnce();
|
||||
$secondResolve->shouldBeCalledOnce();
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function returnsResultOfFirstInnerResolver()
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willReturn([]);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$firstResolve->shouldHaveBeenCalledOnce();
|
||||
$secondResolve->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function returnsResultOfSecondInnerResolver()
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willReturn([]);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$firstResolve->shouldHaveBeenCalledOnce();
|
||||
$secondResolve->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\IpGeolocation;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\EmptyIpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
class EmptyIpLocationResolverTest extends TestCase
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
private const EMPTY_RESP = [
|
||||
'country_code' => '',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'time_zone' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var EmptyIpLocationResolver
|
||||
*/
|
||||
private $resolver;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->resolver = new EmptyIpLocationResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideEmptyResponses
|
||||
*/
|
||||
public function alwaysReturnsAnEmptyResponse(array $expected, string $ipAddress)
|
||||
{
|
||||
$this->assertEquals($expected, $this->resolver->resolveIpLocation($ipAddress));
|
||||
}
|
||||
|
||||
public function provideEmptyResponses(): array
|
||||
{
|
||||
return map(range(0, 5), function () {
|
||||
return [self::EMPTY_RESP, $this->generateRandomString(10)];
|
||||
});
|
||||
}
|
||||
}
|
126
module/Common/test/IpGeolocation/GeoLite2/DbUpdaterTest.php
Normal file
126
module/Common/test/IpGeolocation/GeoLite2/DbUpdaterTest.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\IpGeolocation\GeoLite2;
|
||||
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\Diactoros\Response;
|
||||
|
||||
class DbUpdaterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DbUpdater
|
||||
*/
|
||||
private $dbUpdater;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $httpClient;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $filesystem;
|
||||
/**
|
||||
* @var GeoLite2Options
|
||||
*/
|
||||
private $options;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->httpClient = $this->prophesize(ClientInterface::class);
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
$this->options = new GeoLite2Options([
|
||||
'temp_dir' => __DIR__ . '/../../../test-resources',
|
||||
'db_location' => '',
|
||||
'download_from' => '',
|
||||
]);
|
||||
|
||||
$this->dbUpdater = new DbUpdater($this->httpClient->reveal(), $this->filesystem->reveal(), $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeDownloaded()
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'An error occurred while trying to download a fresh copy of the GeoLite2 database'
|
||||
);
|
||||
$request->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeExtracted()
|
||||
{
|
||||
$this->options->tempDir = '__invalid__';
|
||||
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'An error occurred while trying to extract the GeoLite2 database from __invalid__/GeoLite2-City.tar.gz'
|
||||
);
|
||||
$request->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFilesystemExceptions
|
||||
*/
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeCopiedToDestination(string $e)
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willThrow($e);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('An error occurred while trying to copy GeoLite2 db file to destination');
|
||||
$request->shouldBeCalledOnce();
|
||||
$copy->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
public function provideFilesystemExceptions(): array
|
||||
{
|
||||
return [
|
||||
[FilesystemException\FileNotFoundException::class],
|
||||
[FilesystemException\IOException::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noExceptionsAreThrownIfEverythingWorksFine()
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->will(function () {
|
||||
});
|
||||
$remove = $this->filesystem->remove(Argument::cetera())->will(function () {
|
||||
});
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
|
||||
$request->shouldHaveBeenCalledOnce();
|
||||
$copy->shouldHaveBeenCalledOnce();
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\IpGeolocation;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use GeoIp2\Exception\AddressNotFoundException;
|
||||
use GeoIp2\Model\City;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2LocationResolver;
|
||||
|
||||
class GeoLite2LocationResolverTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var GeoLite2LocationResolver
|
||||
*/
|
||||
private $resolver;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $reader;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->reader = $this->prophesize(Reader::class);
|
||||
$this->resolver = new GeoLite2LocationResolver($this->reader->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideReaderExceptions
|
||||
*/
|
||||
public function exceptionIsThrownIfReaderThrowsException(string $e, string $message)
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$cityMethod = $this->reader->city($ipAddress)->willThrow($e);
|
||||
|
||||
$this->expectException(WrongIpException::class);
|
||||
$this->expectExceptionMessage($message);
|
||||
$cityMethod->shouldBeCalledOnce();
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
}
|
||||
|
||||
public function provideReaderExceptions(): array
|
||||
{
|
||||
return [
|
||||
[AddressNotFoundException::class, 'Provided IP "1.2.3.4" is invalid'],
|
||||
[InvalidDatabaseException::class, 'Provided GeoLite2 db file is invalid'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function resolvedCityIsProperlyMapped()
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
$city = new City([]);
|
||||
|
||||
$cityMethod = $this->reader->city($ipAddress)->willReturn($city);
|
||||
|
||||
$result = $this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$this->assertEquals([
|
||||
'country_code' => '',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'time_zone' => '',
|
||||
], $result);
|
||||
$cityMethod->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Service;
|
||||
namespace ShlinkioTest\Shlink\Common\IpGeolocation;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use function json_encode;
|
||||
|
||||
class IpApiLocationResolverTest extends TestCase
|
||||
|
@ -52,7 +52,7 @@ class IpApiLocationResolverTest extends TestCase
|
|||
$response->getBody()->rewind();
|
||||
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willReturn($response)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
||||
}
|
||||
|
||||
|
@ -63,23 +63,7 @@ class IpApiLocationResolverTest extends TestCase
|
|||
public function guzzleExceptionThrowsShlinkException()
|
||||
{
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willThrow(new TransferException())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function getApiIntervalReturnsExpectedValue()
|
||||
{
|
||||
$this->assertEquals(65, $this->ipResolver->getApiInterval());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function getApiLimitReturnsExpectedValue()
|
||||
{
|
||||
$this->assertEquals(145, $this->ipResolver->getApiLimit());
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ class PaginableRepositoryAdapterTest extends TestCase
|
|||
*/
|
||||
public function getItemsFallbacksToFindList()
|
||||
{
|
||||
$this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledTimes(1);
|
||||
$this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledOnce();
|
||||
$this->adapter->getItems(5, 10);
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ class PaginableRepositoryAdapterTest extends TestCase
|
|||
*/
|
||||
public function countFallbacksToCountList()
|
||||
{
|
||||
$this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledTimes(1);
|
||||
$this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledOnce();
|
||||
$this->adapter->count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ class PreviewGeneratorTest extends TestCase
|
|||
{
|
||||
$url = 'http://foo.com';
|
||||
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(true)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->image->saveAs(Argument::cetera())->shouldBeCalledTimes(0);
|
||||
$this->assertEquals(sprintf('dir/preview_%s.png', urlencode($url)), $this->generator->generatePreview($url));
|
||||
}
|
||||
|
@ -65,10 +65,10 @@ class PreviewGeneratorTest extends TestCase
|
|||
$expectedPath = 'dir/' . $cacheId;
|
||||
|
||||
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
|
||||
$this->image->getError()->willReturn('')->shouldBeCalledTimes(1);
|
||||
$this->image->saveAs($expectedPath)->shouldBeCalledOnce();
|
||||
$this->image->getError()->willReturn('')->shouldBeCalledOnce();
|
||||
$this->assertEquals($expectedPath, $this->generator->generatePreview($url));
|
||||
}
|
||||
|
||||
|
@ -83,10 +83,10 @@ class PreviewGeneratorTest extends TestCase
|
|||
$expectedPath = 'dir/' . $cacheId;
|
||||
|
||||
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
|
||||
$this->image->getError()->willReturn('Error!!')->shouldBeCalledTimes(1);
|
||||
$this->image->saveAs($expectedPath)->shouldBeCalledOnce();
|
||||
$this->image->getError()->willReturn('Error!!')->shouldBeCalledOnce();
|
||||
|
||||
$this->generator->generatePreview($url);
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ class TranslatorExtensionTest extends TestCase
|
|||
{
|
||||
$engine = $this->prophesize(Engine::class);
|
||||
|
||||
$engine->registerFunction('translate', Argument::type('callable'))->shouldBeCalledTimes(1);
|
||||
$engine->registerFunction('translate_plural', Argument::type('callable'))->shouldBeCalledTimes(1);
|
||||
$engine->registerFunction('translate', Argument::type('callable'))->shouldBeCalledOnce();
|
||||
$engine->registerFunction('translate_plural', Argument::type('callable'))->shouldBeCalledOnce();
|
||||
|
||||
$funcs = $this->extension->register($engine->reveal());
|
||||
}
|
||||
|
|
|
@ -51,8 +51,8 @@ class PixelActionTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(
|
||||
new ShortUrl('http://domain.com/foo/bar')
|
||||
)->shouldBeCalledTimes(1);
|
||||
$this->visitTracker->track(Argument::cetera())->shouldBeCalledTimes(1);
|
||||
)->shouldBeCalledOnce();
|
||||
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
|
||||
|
|
|
@ -50,9 +50,9 @@ class PreviewActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$delegate->handle(Argument::cetera())->shouldBeCalledTimes(1)
|
||||
$delegate->handle(Argument::cetera())->shouldBeCalledOnce()
|
||||
->willReturn(new Response());
|
||||
|
||||
$this->action->process(
|
||||
|
@ -70,8 +70,8 @@ class PreviewActionTest extends TestCase
|
|||
$url = 'foobar.com';
|
||||
$shortUrl = new ShortUrl($url);
|
||||
$path = __FILE__;
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1);
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledOnce();
|
||||
|
||||
$resp = $this->action->process(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||
|
@ -89,7 +89,7 @@ class PreviewActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
/** @var MethodProphecy $process */
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
@ -99,6 +99,6 @@ class PreviewActionTest extends TestCase
|
|||
$delegate->reveal()
|
||||
);
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class QrCodeActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
||||
|
@ -55,7 +55,7 @@ class QrCodeActionTest extends TestCase
|
|||
$delegate->reveal()
|
||||
);
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,7 +65,7 @@ class QrCodeActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
/** @var MethodProphecy $process */
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
@ -75,7 +75,7 @@ class QrCodeActionTest extends TestCase
|
|||
$delegate->reveal()
|
||||
);
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,7 +85,7 @@ class QrCodeActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(new ShortUrl(''))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
|
||||
$resp = $this->action->process(
|
||||
|
|
|
@ -59,8 +59,8 @@ class RedirectActionTest extends TestCase
|
|||
$expectedUrl = 'http://domain.com/foo/bar';
|
||||
$shortUrl = new ShortUrl($expectedUrl);
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->visitTracker->track(Argument::cetera())->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
|
||||
|
@ -78,7 +78,7 @@ class RedirectActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->visitTracker->track(Argument::cetera())->shouldNotBeCalled();
|
||||
|
||||
$handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
|
@ -87,7 +87,7 @@ class RedirectActionTest extends TestCase
|
|||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$this->action->process($request, $handler->reveal());
|
||||
|
||||
$handle->shouldHaveBeenCalledTimes(1);
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +111,7 @@ class RedirectActionTest extends TestCase
|
|||
|
||||
$this->assertEquals(302, $resp->getStatusCode());
|
||||
$this->assertEquals('https://shlink.io', $resp->getHeaderLine('Location'));
|
||||
$shortCodeToUrl->shouldHaveBeenCalledTimes(1);
|
||||
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
||||
$handle->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ class RedirectActionTest extends TestCase
|
|||
$expectedUrl = 'http://domain.com/foo/bar';
|
||||
$shortUrl = new ShortUrl($expectedUrl);
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->visitTracker->track(Argument::cetera())->shouldNotBeCalled();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
|
||||
|
|
|
@ -36,7 +36,7 @@ class QrCodeCacheMiddlewareTest extends TestCase
|
|||
public function noCachedPathFallsBackToNextMiddleware()
|
||||
{
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1);
|
||||
$delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledOnce();
|
||||
|
||||
$this->middleware->process(ServerRequestFactory::fromGlobals()->withUri(
|
||||
new Uri('/foo/bar')
|
||||
|
|
|
@ -70,8 +70,8 @@ class DeleteShortUrlServiceTest extends TestCase
|
|||
|
||||
$service->deleteByShortCode('abc123', true);
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
$flush->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,8 +86,8 @@ class DeleteShortUrlServiceTest extends TestCase
|
|||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
$flush->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,8 +102,8 @@ class DeleteShortUrlServiceTest extends TestCase
|
|||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
$flush->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
private function createService(bool $checkVisitsThreshold = true, int $visitsThreshold = 5): DeleteShortUrlService
|
||||
|
|
|
@ -49,8 +49,8 @@ class ShortUrlServiceTest extends TestCase
|
|||
];
|
||||
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$repo->findList(Argument::cetera())->willReturn($list)->shouldBeCalledTimes(1);
|
||||
$repo->countList(Argument::cetera())->willReturn(count($list))->shouldBeCalledTimes(1);
|
||||
$repo->findList(Argument::cetera())->willReturn($list)->shouldBeCalledOnce();
|
||||
$repo->countList(Argument::cetera())->willReturn(count($list))->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$list = $this->service->listShortUrls();
|
||||
|
@ -65,7 +65,7 @@ class ShortUrlServiceTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(null)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->expectException(InvalidShortCodeException::class);
|
||||
|
@ -78,16 +78,16 @@ class ShortUrlServiceTest extends TestCase
|
|||
public function providedTagsAreGetFromRepoAndSetToTheShortUrl()
|
||||
{
|
||||
$shortUrl = $this->prophesize(ShortUrl::class);
|
||||
$shortUrl->setTags(Argument::any())->shouldBeCalledTimes(1);
|
||||
$shortUrl->setTags(Argument::any())->shouldBeCalledOnce();
|
||||
$shortCode = 'abc123';
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl->reveal())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$tagRepo = $this->prophesize(EntityRepository::class);
|
||||
$tagRepo->findOneBy(['name' => 'foo'])->willReturn(new Tag('foo'))->shouldbeCalledTimes(1);
|
||||
$tagRepo->findOneBy(['name' => 'bar'])->willReturn(null)->shouldbeCalledTimes(1);
|
||||
$tagRepo->findOneBy(['name' => 'foo'])->willReturn(new Tag('foo'))->shouldBeCalledOnce();
|
||||
$tagRepo->findOneBy(['name' => 'bar'])->willReturn(null)->shouldBeCalledOnce();
|
||||
$this->em->getRepository(Tag::class)->willReturn($tagRepo->reveal());
|
||||
|
||||
$this->service->setTagsByShortCode($shortCode, ['foo', 'bar']);
|
||||
|
|
|
@ -98,8 +98,8 @@ class UrlShortenerTest extends TestCase
|
|||
$conn = $this->prophesize(Connection::class);
|
||||
$conn->isTransactionActive()->willReturn(true);
|
||||
$this->em->getConnection()->willReturn($conn->reveal());
|
||||
$this->em->rollback()->shouldBeCalledTimes(1);
|
||||
$this->em->close()->shouldBeCalledTimes(1);
|
||||
$this->em->rollback()->shouldBeCalledOnce();
|
||||
$this->em->close()->shouldBeCalledOnce();
|
||||
|
||||
$this->em->flush()->willThrow(new ORMException());
|
||||
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||
|
@ -135,7 +135,7 @@ class UrlShortenerTest extends TestCase
|
|||
'custom-slug'
|
||||
);
|
||||
|
||||
$slugify->shouldHaveBeenCalledTimes(1);
|
||||
$slugify->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -153,8 +153,8 @@ class UrlShortenerTest extends TestCase
|
|||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$slugify->shouldBeCalledTimes(1);
|
||||
$findBySlug->shouldBeCalledTimes(1);
|
||||
$slugify->shouldBeCalledOnce();
|
||||
$findBySlug->shouldBeCalledOnce();
|
||||
$getRepo->shouldBeCalled();
|
||||
$this->expectException(NonUniqueSlugException::class);
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ class VisitServiceTest extends TestCase
|
|||
public function saveVisitsPersistsProvidedVisit()
|
||||
{
|
||||
$visit = new Visit(new ShortUrl(''), Visitor::emptyInstance());
|
||||
$this->em->persist($visit)->shouldBeCalledTimes(1);
|
||||
$this->em->flush()->shouldBeCalledTimes(1);
|
||||
$this->em->persist($visit)->shouldBeCalledOnce();
|
||||
$this->em->flush()->shouldBeCalledOnce();
|
||||
$this->visitService->saveVisit($visit);
|
||||
}
|
||||
|
||||
|
@ -46,8 +46,8 @@ class VisitServiceTest extends TestCase
|
|||
public function getUnlocatedVisitsFallbacksToRepository()
|
||||
{
|
||||
$repo = $this->prophesize(VisitRepository::class);
|
||||
$repo->findUnlocatedVisits()->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||
$repo->findUnlocatedVisits()->shouldBeCalledOnce();
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
|
||||
$this->visitService->getUnlocatedVisits();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,9 +40,9 @@ class VisitsTrackerTest extends TestCase
|
|||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl(''));
|
||||
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
|
||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
|
||||
$this->em->persist(Argument::any())->shouldBeCalledOnce();
|
||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledOnce();
|
||||
|
||||
$this->visitsTracker->track($shortCode, Visitor::emptyInstance());
|
||||
}
|
||||
|
@ -57,13 +57,13 @@ class VisitsTrackerTest extends TestCase
|
|||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl(''));
|
||||
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
|
||||
$this->em->persist(Argument::any())->will(function ($args) use ($test) {
|
||||
/** @var Visit $visit */
|
||||
$visit = $args[0];
|
||||
$test->assertEquals('4.3.2.0', $visit->getRemoteAddr());
|
||||
})->shouldBeCalledTimes(1);
|
||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
||||
})->shouldBeCalledOnce();
|
||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledOnce();
|
||||
|
||||
$this->visitsTracker->track($shortCode, new Visitor('', '', '4.3.2.1'));
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ class VisitsTrackerTest extends TestCase
|
|||
$shortUrl = new ShortUrl('http://domain.com/foo/bar');
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
|
||||
|
||||
$list = [
|
||||
new Visit(new ShortUrl(''), Visitor::emptyInstance()),
|
||||
|
@ -85,7 +85,7 @@ class VisitsTrackerTest extends TestCase
|
|||
];
|
||||
$repo2 = $this->prophesize(VisitRepository::class);
|
||||
$repo2->findVisitsByShortUrl($shortUrl, null)->willReturn($list);
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
|
||||
|
||||
$this->assertEquals($list, $this->visitsTracker->info($shortCode));
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ use Shlinkio\Shlink\Installer\Config\Plugin;
|
|||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
|
@ -149,7 +148,7 @@ class InstallCommand extends Command
|
|||
// If current command is not update, generate database
|
||||
if (! $this->isUpdate) {
|
||||
$this->io->write('Initializing database...');
|
||||
if (! $this->runPhpCommand(
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
|
||||
'Error generating database.',
|
||||
$output
|
||||
|
@ -160,7 +159,7 @@ class InstallCommand extends Command
|
|||
|
||||
// Run database migrations
|
||||
$this->io->write('Updating database...');
|
||||
if (! $this->runPhpCommand(
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
|
||||
'Error updating database.',
|
||||
$output
|
||||
|
@ -170,7 +169,7 @@ class InstallCommand extends Command
|
|||
|
||||
// Generate proxies
|
||||
$this->io->write('Generating proxies...');
|
||||
if (! $this->runPhpCommand(
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
|
||||
'Error generating proxies.',
|
||||
$output
|
||||
|
@ -178,6 +177,12 @@ class InstallCommand extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
// Download GeoLite2 db filte
|
||||
$this->io->write('Downloading GeoLite2 db...');
|
||||
if (! $this->execPhp('bin/cli visit:update-db', 'Error downloading GeoLite2 db.', $output)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->io->success('Installation complete!');
|
||||
}
|
||||
|
||||
|
@ -226,15 +231,7 @@ class InstallCommand extends Command
|
|||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param string $errorMessage
|
||||
* @param OutputInterface $output
|
||||
* @return bool
|
||||
* @throws LogicException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
|
||||
private function execPhp(string $command, string $errorMessage, OutputInterface $output): bool
|
||||
{
|
||||
if ($this->processHelper === null) {
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
|
@ -256,7 +253,7 @@ class InstallCommand extends Command
|
|||
}
|
||||
|
||||
if (! $this->io->isVerbose()) {
|
||||
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
|
||||
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -81,7 +81,7 @@ class InstallCommandTest extends TestCase
|
|||
*/
|
||||
public function generatedConfigIsProperlyPersisted()
|
||||
{
|
||||
$this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1);
|
||||
$this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledOnce();
|
||||
$this->commandTester->execute([]);
|
||||
}
|
||||
|
||||
|
@ -97,8 +97,8 @@ class InstallCommandTest extends TestCase
|
|||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$appConfigExists->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigRemove->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigExists->shouldHaveBeenCalledOnce();
|
||||
$appConfigRemove->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,8 +115,8 @@ class InstallCommandTest extends TestCase
|
|||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$appConfigExists->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigRemove->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigExists->shouldHaveBeenCalledOnce();
|
||||
$appConfigRemove->shouldHaveBeenCalledOnce();
|
||||
$configToFile->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class ApplicationConfigCustomizerTest extends TestCase
|
|||
'CHECK_VISITS_THRESHOLD' => false,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,7 +77,7 @@ class ApplicationConfigCustomizerTest extends TestCase
|
|||
'VISITS_THRESHOLD' => 20,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +101,7 @@ class ApplicationConfigCustomizerTest extends TestCase
|
|||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -55,7 +55,7 @@ class DatabaseConfigCustomizerTest extends TestCase
|
|||
'HOST' => 'param',
|
||||
'PORT' => 'param',
|
||||
], $config->getDatabase());
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledOnce();
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,6 @@ class DatabaseConfigCustomizerTest extends TestCase
|
|||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
], $config->getDatabase());
|
||||
$copy->shouldHaveBeenCalledTimes(1);
|
||||
$copy->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ class LanguageConfigCustomizerTest extends TestCase
|
|||
'DEFAULT' => 'en',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -50,7 +50,7 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
|||
'NOT_FOUND_REDIRECT_TO' => 'asked',
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledOnce();
|
||||
$confirm->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
|
@ -81,8 +81,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
|||
'NOT_FOUND_REDIRECT_TO' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledOnce();
|
||||
$confirm->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -146,6 +146,6 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
|||
'ENABLE_NOT_FOUND_REDIRECTION' => false,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ class AuthenticateActionTest extends TestCase
|
|||
public function properApiKeyReturnsTokenInResponse()
|
||||
{
|
||||
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId('5'))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'apiKey' => 'foo',
|
||||
|
@ -75,7 +75,7 @@ class AuthenticateActionTest extends TestCase
|
|||
public function invalidApiKeyReturnsErrorResponse()
|
||||
{
|
||||
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->disable())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'apiKey' => 'foo',
|
||||
|
|
|
@ -56,7 +56,7 @@ class CreateShortUrlActionTest extends TestCase
|
|||
->willReturn(
|
||||
(new ShortUrl(''))->setShortCode('abc123')
|
||||
)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||
|
@ -73,7 +73,7 @@ class CreateShortUrlActionTest extends TestCase
|
|||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera())
|
||||
->willThrow(InvalidUrlException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||
|
@ -95,7 +95,7 @@ class CreateShortUrlActionTest extends TestCase
|
|||
null,
|
||||
'foo',
|
||||
Argument::cetera()
|
||||
)->willThrow(NonUniqueSlugException::class)->shouldBeCalledTimes(1);
|
||||
)->willThrow(NonUniqueSlugException::class)->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||
|
@ -113,7 +113,7 @@ class CreateShortUrlActionTest extends TestCase
|
|||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera())
|
||||
->willThrow(Exception::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||
|
|
|
@ -43,7 +43,7 @@ class DeleteShortUrlActionTest extends TestCase
|
|||
$resp = $this->action->handle(ServerRequestFactory::fromGlobals());
|
||||
|
||||
$this->assertEquals(204, $resp->getStatusCode());
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +60,7 @@ class DeleteShortUrlActionTest extends TestCase
|
|||
|
||||
$this->assertEquals($statusCode, $resp->getStatusCode());
|
||||
$this->assertEquals($error, $payload['error']);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExceptions(): array
|
||||
|
|
|
@ -45,7 +45,7 @@ class EditShortUrlTagsActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
|
||||
|
@ -61,7 +61,7 @@ class EditShortUrlTagsActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl(''))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
|
||||
|
|
|
@ -40,7 +40,7 @@ class ListShortUrlsActionTest extends TestCase
|
|||
{
|
||||
$page = 3;
|
||||
$this->service->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
|
||||
'page' => $page,
|
||||
|
@ -55,7 +55,7 @@ class ListShortUrlsActionTest extends TestCase
|
|||
{
|
||||
$page = 3;
|
||||
$this->service->listShortUrls($page, null, [], null)->willThrow(Exception::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
|
||||
'page' => $page,
|
||||
|
|
|
@ -40,7 +40,7 @@ class ResolveShortUrlActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->handle($request);
|
||||
|
@ -56,7 +56,7 @@ class ResolveShortUrlActionTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(
|
||||
new ShortUrl('http://domain.com/foo/bar')
|
||||
)->shouldBeCalledTimes(1);
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->handle($request);
|
||||
|
@ -71,7 +71,7 @@ class ResolveShortUrlActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->handle($request);
|
||||
|
@ -86,7 +86,7 @@ class ResolveShortUrlActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(Exception::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->handle($request);
|
||||
|
|
|
@ -39,7 +39,7 @@ class GetVisitsActionTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
@ -53,7 +53,7 @@ class GetVisitsActionTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow(
|
||||
InvalidArgumentException::class
|
||||
)->shouldBeCalledTimes(1);
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
|
@ -67,7 +67,7 @@ class GetVisitsActionTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow(
|
||||
Exception::class
|
||||
)->shouldBeCalledTimes(1);
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
|
||||
$this->assertEquals(500, $response->getStatusCode());
|
||||
|
@ -81,7 +81,7 @@ class GetVisitsActionTest extends TestCase
|
|||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(null, Chronos::parse('2016-01-01 00:00:00')))
|
||||
->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
|
||||
|
|
|
@ -37,7 +37,7 @@ class ApiKeyHeaderPluginTest extends TestCase
|
|||
{
|
||||
$apiKey = 'abc-ABC';
|
||||
$check = $this->apiKeyService->check($apiKey)->willReturn(false);
|
||||
$check->shouldBeCalledTimes(1);
|
||||
$check->shouldBeCalledOnce();
|
||||
|
||||
$this->expectException(VerifyAuthenticationException::class);
|
||||
$this->expectExceptionMessage('Provided API key does not exist or is invalid');
|
||||
|
@ -55,7 +55,7 @@ class ApiKeyHeaderPluginTest extends TestCase
|
|||
|
||||
$this->plugin->verify($this->createRequest($apiKey));
|
||||
|
||||
$check->shouldHaveBeenCalledTimes(1);
|
||||
$check->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -90,7 +90,7 @@ class AuthorizationHeaderPluginTest extends TestCase
|
|||
|
||||
$this->plugin->verify($request);
|
||||
|
||||
$jwtVerify->shouldHaveBeenCalledTimes(1);
|
||||
$jwtVerify->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,7 +107,7 @@ class AuthorizationHeaderPluginTest extends TestCase
|
|||
|
||||
$this->plugin->verify($request);
|
||||
|
||||
$jwtVerify->shouldHaveBeenCalledTimes(1);
|
||||
$jwtVerify->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,6 +126,6 @@ class AuthorizationHeaderPluginTest extends TestCase
|
|||
|
||||
$this->assertTrue($response->hasHeader(AuthorizationHeaderPlugin::HEADER_NAME));
|
||||
$this->assertEquals('Bearer DEF-def', $response->getHeaderLine(AuthorizationHeaderPlugin::HEADER_NAME));
|
||||
$jwtRefresh->shouldHaveBeenCalledTimes(1);
|
||||
$jwtRefresh->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ class RequestToAuthPluginTest extends TestCase
|
|||
|
||||
$this->requestToPlugin->fromRequest($request);
|
||||
|
||||
$getPlugin->shouldHaveBeenCalledTimes(1);
|
||||
$getPlugin->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideHeaders(): array
|
||||
|
|
|
@ -68,7 +68,7 @@ class AuthenticationMiddlewareTest extends TestCase
|
|||
|
||||
$this->middleware->process($request, $handler->reveal());
|
||||
|
||||
$handle->shouldHaveBeenCalledTimes(1);
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
$fromRequest->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ class AuthenticationMiddlewareTest extends TestCase
|
|||
'Expected one of the following authentication headers, but none were provided, ["%s"]',
|
||||
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
|
||||
), $payload['message']);
|
||||
$fromRequest->shouldHaveBeenCalledTimes(1);
|
||||
$fromRequest->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExceptions(): array
|
||||
|
@ -150,8 +150,8 @@ class AuthenticationMiddlewareTest extends TestCase
|
|||
|
||||
$this->assertEquals('the_error', $payload['error']);
|
||||
$this->assertEquals('the_message', $payload['message']);
|
||||
$verify->shouldHaveBeenCalledTimes(1);
|
||||
$fromRequest->shouldHaveBeenCalledTimes(1);
|
||||
$verify->shouldHaveBeenCalledOnce();
|
||||
$fromRequest->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,10 +176,10 @@ class AuthenticationMiddlewareTest extends TestCase
|
|||
$response = $this->middleware->process($request, $handler->reveal());
|
||||
|
||||
$this->assertSame($response, $newResponse);
|
||||
$verify->shouldHaveBeenCalledTimes(1);
|
||||
$update->shouldHaveBeenCalledTimes(1);
|
||||
$handle->shouldHaveBeenCalledTimes(1);
|
||||
$fromRequest->shouldHaveBeenCalledTimes(1);
|
||||
$verify->shouldHaveBeenCalledOnce();
|
||||
$update->shouldHaveBeenCalledOnce();
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
$fromRequest->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
private function getDummyMiddleware(): MiddlewareInterface
|
||||
|
|
|
@ -38,7 +38,7 @@ class BodyParserMiddlewareTest extends TestCase
|
|||
|
||||
$this->middleware->process($request, $delegate->reveal());
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +70,7 @@ class BodyParserMiddlewareTest extends TestCase
|
|||
|
||||
$this->middleware->process($request, $delegate->reveal());
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,6 +101,6 @@ class BodyParserMiddlewareTest extends TestCase
|
|||
|
||||
$this->middleware->process($request, $delegate->reveal());
|
||||
|
||||
$process->shouldHaveBeenCalledTimes(1);
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class CrossDomainMiddlewareTest extends TestCase
|
|||
public function nonCrossDomainRequestsAreNotAffected()
|
||||
{
|
||||
$originalResponse = new Response();
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->delegate->reveal());
|
||||
$this->assertSame($originalResponse, $response);
|
||||
|
@ -50,7 +50,7 @@ class CrossDomainMiddlewareTest extends TestCase
|
|||
public function anyRequestIncludesTheAllowAccessHeader()
|
||||
{
|
||||
$originalResponse = new Response();
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->middleware->process(
|
||||
ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'),
|
||||
|
@ -70,7 +70,7 @@ class CrossDomainMiddlewareTest extends TestCase
|
|||
{
|
||||
$originalResponse = new Response();
|
||||
$request = ServerRequestFactory::fromGlobals()->withMethod('OPTIONS')->withHeader('Origin', 'local');
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
|
||||
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->middleware->process($request, $this->delegate->reveal());
|
||||
$this->assertNotSame($originalResponse, $response);
|
||||
|
|
|
@ -45,6 +45,6 @@ class ShortCodePathMiddlewareTest extends TestCase
|
|||
|
||||
$this->middleware->process($request->reveal(), $this->requestHandler->reveal());
|
||||
|
||||
$withUri->shouldHaveBeenCalledTimes(1);
|
||||
$withUri->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ class ApiKeyServiceTest extends TestCase
|
|||
*/
|
||||
public function keyIsProperlyCreated()
|
||||
{
|
||||
$this->em->flush()->shouldBeCalledTimes(1);
|
||||
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1);
|
||||
$this->em->flush()->shouldBeCalledOnce();
|
||||
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledOnce();
|
||||
|
||||
$key = $this->service->create();
|
||||
$this->assertNull($key->getExpirationDate());
|
||||
|
@ -46,8 +46,8 @@ class ApiKeyServiceTest extends TestCase
|
|||
*/
|
||||
public function keyIsProperlyCreatedWithExpirationDate()
|
||||
{
|
||||
$this->em->flush()->shouldBeCalledTimes(1);
|
||||
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1);
|
||||
$this->em->flush()->shouldBeCalledOnce();
|
||||
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledOnce();
|
||||
|
||||
$date = Chronos::parse('2030-01-01');
|
||||
$key = $this->service->create($date);
|
||||
|
@ -61,7 +61,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
{
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn(null)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->assertFalse($this->service->check('12345'));
|
||||
|
@ -76,7 +76,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
$key->disable();
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->assertFalse($this->service->check('12345'));
|
||||
|
@ -90,7 +90,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
$key = new ApiKey(Chronos::now()->subDay());
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->assertFalse($this->service->check('12345'));
|
||||
|
@ -103,7 +103,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
{
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn(new ApiKey())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->assertTrue($this->service->check('12345'));
|
||||
|
@ -117,7 +117,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
{
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn(null)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->service->disable('12345');
|
||||
|
@ -131,10 +131,10 @@ class ApiKeyServiceTest extends TestCase
|
|||
$key = new ApiKey();
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->em->flush()->shouldBeCalledTimes(1);
|
||||
$this->em->flush()->shouldBeCalledOnce();
|
||||
|
||||
$this->assertTrue($key->isEnabled());
|
||||
$returnedKey = $this->service->disable('12345');
|
||||
|
@ -149,7 +149,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
{
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findBy([])->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->service->listKeys();
|
||||
|
@ -162,7 +162,7 @@ class ApiKeyServiceTest extends TestCase
|
|||
{
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findBy(['enabled' => true])->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->service->listKeys(true);
|
||||
|
|
Loading…
Add table
Reference in a new issue