mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-22 08:56:42 -06:00
Defined stricter model to represent one geo location
This commit is contained in:
parent
5c5dde48de
commit
e2abe23895
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
@ -75,7 +76,7 @@ class ProcessVisitsCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
public function getGeolocationDataForVisit(Visit $visit): array
|
||||
public function getGeolocationDataForVisit(Visit $visit): Location
|
||||
{
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$this->output->writeln(
|
||||
|
@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
@ -75,16 +76,14 @@ class GetVisitsCommandTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function outputIsProperlyGenerated()
|
||||
/** @test */
|
||||
public function outputIsProperlyGenerated(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn(
|
||||
new Paginator(new ArrayAdapter([
|
||||
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
|
||||
new VisitLocation(['country_name' => 'Spain'])
|
||||
new VisitLocation(new Location('', 'Spain', '', '', 0, 0, ''))
|
||||
),
|
||||
]))
|
||||
)->shouldBeCalledOnce();
|
||||
|
@ -9,18 +9,17 @@ use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
use function array_shift;
|
||||
use function sprintf;
|
||||
|
||||
@ -60,13 +59,11 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function allPendingVisitsAreProcessed()
|
||||
/** @test */
|
||||
public function allPendingVisitsAreProcessed(): void
|
||||
{
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
|
||||
$location = new VisitLocation([]);
|
||||
$location = new VisitLocation(Location::emptyInstance());
|
||||
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
@ -77,7 +74,9 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
$secondCallback($location, $visit);
|
||||
}
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
||||
Location::emptyInstance()
|
||||
);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
@ -93,10 +92,10 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
* @test
|
||||
* @dataProvider provideIgnoredAddresses
|
||||
*/
|
||||
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message)
|
||||
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
|
||||
{
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
|
||||
$location = new VisitLocation([]);
|
||||
$location = new VisitLocation(Location::emptyInstance());
|
||||
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
@ -107,39 +106,32 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
$secondCallback($location, $visit);
|
||||
}
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
||||
Location::emptyInstance()
|
||||
);
|
||||
|
||||
try {
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
} catch (Throwable $e) {
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
|
||||
|
||||
$this->assertStringContainsString($message, $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString($message, $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideIgnoredAddresses(): array
|
||||
public function provideIgnoredAddresses(): iterable
|
||||
{
|
||||
return [
|
||||
['', 'Ignored visit with no IP address'],
|
||||
[null, 'Ignored visit with no IP address'],
|
||||
[IpAddress::LOCALHOST, 'Ignored localhost address'],
|
||||
];
|
||||
yield 'with empty address' => ['', 'Ignored visit with no IP address'];
|
||||
yield 'with null address' => [null, 'Ignored visit with no IP address'];
|
||||
yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorWhileLocatingIpIsDisplayed()
|
||||
/** @test */
|
||||
public function errorWhileLocatingIpIsDisplayed(): void
|
||||
{
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
|
||||
$location = new VisitLocation([]);
|
||||
$location = new VisitLocation(Location::emptyInstance());
|
||||
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
@ -152,19 +144,15 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
|
||||
|
||||
try {
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
} catch (Throwable $e) {
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('An error occurred while locating IP. Skipped', $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
$this->assertStringContainsString('An error occurred while locating IP. Skipped', $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,7 +18,7 @@ class ChainIpLocationResolver implements IpLocationResolverInterface
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
$error = null;
|
||||
|
||||
|
@ -10,16 +10,8 @@ class EmptyIpLocationResolver implements IpLocationResolverInterface
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
return [
|
||||
'country_code' => '',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'time_zone' => '',
|
||||
];
|
||||
return Model\Location::emptyInstance();
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
try {
|
||||
$city = $this->geoLiteDbReader->city($ipAddress);
|
||||
@ -36,19 +36,19 @@ class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(City $city): array
|
||||
private function mapFields(City $city): Model\Location
|
||||
{
|
||||
/** @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 ?? '',
|
||||
];
|
||||
return new Model\Location(
|
||||
$city->country->isoCode ?? '',
|
||||
$city->country->name ?? '',
|
||||
$region->name ?? '',
|
||||
$city->city->name ?? '',
|
||||
(float) ($city->location->latitude ?? ''),
|
||||
(float) ($city->location->longitude ?? ''),
|
||||
$city->location->timeZone ?? ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
@ -37,16 +37,16 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(array $entry): array
|
||||
private function mapFields(array $entry): Model\Location
|
||||
{
|
||||
return [
|
||||
'country_code' => $entry['countryCode'] ?? '',
|
||||
'country_name' => $entry['country'] ?? '',
|
||||
'region_name' => $entry['regionName'] ?? '',
|
||||
'city' => $entry['city'] ?? '',
|
||||
'latitude' => $entry['lat'] ?? '',
|
||||
'longitude' => $entry['lon'] ?? '',
|
||||
'time_zone' => $entry['timezone'] ?? '',
|
||||
];
|
||||
return new Model\Location(
|
||||
(string) ($entry['countryCode'] ?? ''),
|
||||
(string) ($entry['country'] ?? ''),
|
||||
(string) ($entry['regionName'] ?? ''),
|
||||
(string) ($entry['city'] ?? ''),
|
||||
(float) ($entry['lat'] ?? 0.0),
|
||||
(float) ($entry['lon'] ?? 0.0),
|
||||
(string) ($entry['timezone'] ?? '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,5 +10,5 @@ interface IpLocationResolverInterface
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array;
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location;
|
||||
}
|
||||
|
80
module/Common/src/IpGeolocation/Model/Location.php
Normal file
80
module/Common/src/IpGeolocation/Model/Location.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\IpGeolocation\Model;
|
||||
|
||||
final class Location
|
||||
{
|
||||
/** @var string */
|
||||
private $countryCode;
|
||||
/** @var string */
|
||||
private $countryName;
|
||||
/** @var string */
|
||||
private $regionName;
|
||||
/** @var string */
|
||||
private $city;
|
||||
/** @var float */
|
||||
private $latitude;
|
||||
/** @var float */
|
||||
private $longitude;
|
||||
/** @var string */
|
||||
private $timeZone;
|
||||
|
||||
public function __construct(
|
||||
string $countryCode,
|
||||
string $countryName,
|
||||
string $regionName,
|
||||
string $city,
|
||||
float $latitude,
|
||||
float $longitude,
|
||||
string $timeZone
|
||||
) {
|
||||
$this->countryCode = $countryCode;
|
||||
$this->countryName = $countryName;
|
||||
$this->regionName = $regionName;
|
||||
$this->city = $city;
|
||||
$this->latitude = $latitude;
|
||||
$this->longitude = $longitude;
|
||||
$this->timeZone = $timeZone;
|
||||
}
|
||||
|
||||
public static function emptyInstance(): self
|
||||
{
|
||||
return new self('', '', '', '', 0.0, 0.0, '');
|
||||
}
|
||||
|
||||
public function countryCode(): string
|
||||
{
|
||||
return $this->countryCode;
|
||||
}
|
||||
|
||||
public function countryName(): string
|
||||
{
|
||||
return $this->countryName;
|
||||
}
|
||||
|
||||
public function regionName(): string
|
||||
{
|
||||
return $this->regionName;
|
||||
}
|
||||
|
||||
public function city(): string
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
public function latitude(): float
|
||||
{
|
||||
return $this->latitude;
|
||||
}
|
||||
|
||||
public function longitude(): float
|
||||
{
|
||||
return $this->longitude;
|
||||
}
|
||||
|
||||
public function timeZone(): string
|
||||
{
|
||||
return $this->timeZone;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\ChainIpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
|
||||
class ChainIpLocationResolverTest extends TestCase
|
||||
{
|
||||
@ -46,14 +47,12 @@ class ChainIpLocationResolverTest extends TestCase
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function returnsResultOfFirstInnerResolver()
|
||||
/** @test */
|
||||
public function returnsResultOfFirstInnerResolver(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willReturn([]);
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willReturn(Location::emptyInstance());
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
@ -62,15 +61,15 @@ class ChainIpLocationResolverTest extends TestCase
|
||||
$secondResolve->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function returnsResultOfSecondInnerResolver()
|
||||
/** @test */
|
||||
public function returnsResultOfSecondInnerResolver(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willReturn([]);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willReturn(
|
||||
Location::emptyInstance()
|
||||
);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
|
@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Common\IpGeolocation;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\EmptyIpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
@ -13,16 +14,6 @@ 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;
|
||||
|
||||
@ -35,15 +26,15 @@ class EmptyIpLocationResolverTest extends TestCase
|
||||
* @test
|
||||
* @dataProvider provideEmptyResponses
|
||||
*/
|
||||
public function alwaysReturnsAnEmptyResponse(array $expected, string $ipAddress)
|
||||
public function alwaysReturnsAnEmptyResponse(string $ipAddress): void
|
||||
{
|
||||
$this->assertEquals($expected, $this->resolver->resolveIpLocation($ipAddress));
|
||||
$this->assertEquals(Location::emptyInstance(), $this->resolver->resolveIpLocation($ipAddress));
|
||||
}
|
||||
|
||||
public function provideEmptyResponses(): array
|
||||
{
|
||||
return map(range(0, 5), function () {
|
||||
return [self::EMPTY_RESP, $this->generateRandomString(10)];
|
||||
return [$this->generateRandomString(15)];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2LocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
|
||||
class GeoLite2LocationResolverTest extends TestCase
|
||||
{
|
||||
@ -51,10 +52,8 @@ class GeoLite2LocationResolverTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function resolvedCityIsProperlyMapped()
|
||||
/** @test */
|
||||
public function resolvedCityIsProperlyMapped(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
$city = new City([]);
|
||||
@ -63,15 +62,7 @@ class GeoLite2LocationResolverTest extends TestCase
|
||||
|
||||
$result = $this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$this->assertEquals([
|
||||
'country_code' => '',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'time_zone' => '',
|
||||
], $result);
|
||||
$this->assertEquals(Location::emptyInstance(), $result);
|
||||
$cityMethod->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use function json_encode;
|
||||
|
||||
class IpApiLocationResolverTest extends TestCase
|
||||
@ -25,25 +26,15 @@ class IpApiLocationResolverTest extends TestCase
|
||||
$this->ipResolver = new IpApiLocationResolver($this->client->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function correctIpReturnsDecodedInfo()
|
||||
/** @test */
|
||||
public function correctIpReturnsDecodedInfo(): void
|
||||
{
|
||||
$actual = [
|
||||
'countryCode' => 'bar',
|
||||
'lat' => 5,
|
||||
'lon' => 10,
|
||||
];
|
||||
$expected = [
|
||||
'country_code' => 'bar',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => 5,
|
||||
'longitude' => 10,
|
||||
'time_zone' => '',
|
||||
];
|
||||
$expected = new Location('bar', '', '', '', 5, 10, '');
|
||||
$response = new Response();
|
||||
$response->getBody()->write(json_encode($actual));
|
||||
$response->getBody()->rewind();
|
||||
@ -54,7 +45,7 @@ class IpApiLocationResolverTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function guzzleExceptionThrowsShlinkException()
|
||||
public function guzzleExceptionThrowsShlinkException(): void
|
||||
{
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willThrow(new TransferException())
|
||||
->shouldBeCalledOnce();
|
||||
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
||||
{
|
||||
@ -24,9 +24,9 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
||||
/** @var string */
|
||||
private $timezone;
|
||||
|
||||
public function __construct(array $locationInfo)
|
||||
public function __construct(Location $location)
|
||||
{
|
||||
$this->exchangeArray($locationInfo);
|
||||
$this->exchangeLocationInfo($location);
|
||||
}
|
||||
|
||||
public function getCountryName(): string
|
||||
@ -49,32 +49,15 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
||||
return $this->cityName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange internal values from provided array
|
||||
*/
|
||||
private function exchangeArray(array $array): void
|
||||
private function exchangeLocationInfo(Location $info): void
|
||||
{
|
||||
if (array_key_exists('country_code', $array)) {
|
||||
$this->countryCode = (string) $array['country_code'];
|
||||
}
|
||||
if (array_key_exists('country_name', $array)) {
|
||||
$this->countryName = (string) $array['country_name'];
|
||||
}
|
||||
if (array_key_exists('region_name', $array)) {
|
||||
$this->regionName = (string) $array['region_name'];
|
||||
}
|
||||
if (array_key_exists('city', $array)) {
|
||||
$this->cityName = (string) $array['city'];
|
||||
}
|
||||
if (array_key_exists('latitude', $array)) {
|
||||
$this->latitude = (string) $array['latitude'];
|
||||
}
|
||||
if (array_key_exists('longitude', $array)) {
|
||||
$this->longitude = (string) $array['longitude'];
|
||||
}
|
||||
if (array_key_exists('time_zone', $array)) {
|
||||
$this->timezone = (string) $array['time_zone'];
|
||||
}
|
||||
$this->countryCode = $info->countryCode();
|
||||
$this->countryName = $info->countryName();
|
||||
$this->regionName = $info->regionName();
|
||||
$this->cityName = $info->city();
|
||||
$this->latitude = (string) $info->latitude();
|
||||
$this->longitude = (string) $info->longitude();
|
||||
$this->timezone = $info->timeZone();
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
@ -19,7 +20,7 @@ class VisitService implements VisitServiceInterface
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function locateVisits(callable $getGeolocationData, ?callable $locatedVisit = null): void
|
||||
public function locateVisits(callable $geolocateVisit, ?callable $notifyVisitWithLocation = null): void
|
||||
{
|
||||
/** @var VisitRepository $repo */
|
||||
$repo = $this->em->getRepository(Visit::class);
|
||||
@ -27,26 +28,27 @@ class VisitService implements VisitServiceInterface
|
||||
|
||||
foreach ($results as [$visit]) {
|
||||
try {
|
||||
$locationData = $getGeolocationData($visit);
|
||||
/** @var Location $location */
|
||||
$location = $geolocateVisit($visit);
|
||||
} catch (IpCannotBeLocatedException $e) {
|
||||
// Skip if the visit's IP could not be located
|
||||
continue;
|
||||
}
|
||||
|
||||
$location = new VisitLocation($locationData);
|
||||
$this->locateVisit($visit, $location, $locatedVisit);
|
||||
$location = new VisitLocation($location);
|
||||
$this->locateVisit($visit, $location, $notifyVisitWithLocation);
|
||||
}
|
||||
}
|
||||
|
||||
private function locateVisit(Visit $visit, VisitLocation $location, ?callable $locatedVisit): void
|
||||
private function locateVisit(Visit $visit, VisitLocation $location, ?callable $notifyVisitWithLocation): void
|
||||
{
|
||||
$visit->locate($location);
|
||||
|
||||
$this->em->persist($visit);
|
||||
$this->em->flush();
|
||||
|
||||
if ($locatedVisit !== null) {
|
||||
$locatedVisit($location, $visit);
|
||||
if ($notifyVisitWithLocation !== null) {
|
||||
$notifyVisitWithLocation($location, $visit);
|
||||
}
|
||||
|
||||
$this->em->clear();
|
||||
|
@ -5,5 +5,5 @@ namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
interface VisitServiceInterface
|
||||
{
|
||||
public function locateVisits(callable $getGeolocationData, ?callable $locatedVisit = null): void;
|
||||
public function locateVisits(callable $geolocateVisit, ?callable $notifyVisitWithLocation = null): void;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ShlinkioTest\Shlink\Core\Repository;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
@ -29,10 +30,8 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$this->repo = $this->getEntityManager()->getRepository(Visit::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function findUnlocatedVisitsReturnsProperVisits()
|
||||
/** @test */
|
||||
public function findUnlocatedVisitsReturnsProperVisits(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
@ -41,7 +40,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$visit = new Visit($shortUrl, Visitor::emptyInstance());
|
||||
|
||||
if ($i % 2 === 0) {
|
||||
$location = new VisitLocation([]);
|
||||
$location = new VisitLocation(Location::emptyInstance());
|
||||
$this->getEntityManager()->persist($location);
|
||||
$visit->locate($location);
|
||||
}
|
||||
@ -59,10 +58,8 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$this->assertEquals(3, $resultsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function findVisitsByShortCodeReturnsProperData()
|
||||
/** @test */
|
||||
public function findVisitsByShortCodeReturnsProperData(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
@ -86,10 +83,8 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$this->assertCount(2, $this->repo->findVisitsByShortCode($shortUrl->getShortCode(), null, 5, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function countVisitsByShortCodeReturnsProperData()
|
||||
/** @test */
|
||||
public function countVisitsByShortCodeReturnsProperData(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
|
@ -4,20 +4,15 @@ declare(strict_types=1);
|
||||
namespace ShlinkioTest\Shlink\Core\Entity;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
|
||||
class VisitLocationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function valuesFoundWhenExchangingArrayAreCastToString()
|
||||
/** @test */
|
||||
public function valuesFoundWhenExchangingArrayAreCastToString(): void
|
||||
{
|
||||
$payload = [
|
||||
'latitude' => 1000.7,
|
||||
'longitude' => -2000.4,
|
||||
];
|
||||
|
||||
$payload = new Location('', '', '', '', 1000.7, -2000.4, '');
|
||||
$location = new VisitLocation($payload);
|
||||
|
||||
$this->assertSame('1000.7', $location->getLatitude());
|
||||
|
@ -7,6 +7,7 @@ use Doctrine\ORM\EntityManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
@ -31,10 +32,8 @@ class VisitServiceTest extends TestCase
|
||||
$this->visitService = new VisitService($this->em->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function locateVisitsIteratesAndLocatesUnlocatedVisits()
|
||||
/** @test */
|
||||
public function locateVisitsIteratesAndLocatesUnlocatedVisits(): void
|
||||
{
|
||||
$unlocatedVisits = [
|
||||
[new Visit(new ShortUrl('foo'), Visitor::emptyInstance())],
|
||||
@ -53,7 +52,7 @@ class VisitServiceTest extends TestCase
|
||||
});
|
||||
|
||||
$this->visitService->locateVisits(function () {
|
||||
return [];
|
||||
return Location::emptyInstance();
|
||||
}, function () {
|
||||
$args = func_get_args();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user