diff --git a/module/CLI/src/Command/Visit/ProcessVisitsCommand.php b/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
index 215977d8..8de39651 100644
--- a/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
+++ b/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
@@ -65,7 +65,11 @@ class ProcessVisitsCommand extends Command
$this->visitService->locateUnlocatedVisits(
[$this, 'getGeolocationDataForVisit'],
function (VisitLocation $location) use ($output) {
- $output->writeln(sprintf(' [Address located at "%s"]', $location->getCountryName()));
+ if (! $location->isEmpty()) {
+ $output->writeln(
+ sprintf(' [Address located at "%s"]', $location->getCountryName())
+ );
+ }
}
);
diff --git a/module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php b/module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php
index dbb311f2..9c008ed3 100644
--- a/module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php
+++ b/module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php
@@ -116,6 +116,11 @@ class ProcessVisitsCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString($message, $output);
+ if (empty($address)) {
+ $this->assertStringNotContainsString('Processing IP', $output);
+ } else {
+ $this->assertStringContainsString('Processing IP', $output);
+ }
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldNotHaveBeenCalled();
}
diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php
index b8376c01..fad9c986 100644
--- a/module/Core/src/Entity/VisitLocation.php
+++ b/module/Core/src/Entity/VisitLocation.php
@@ -72,4 +72,16 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
'timezone' => $this->timezone,
];
}
+
+ public function isEmpty(): bool
+ {
+ return
+ $this->countryCode === '' &&
+ $this->countryName === '' &&
+ $this->regionName === '' &&
+ $this->cityName === '' &&
+ ((float) $this->latitude) === 0.0 &&
+ ((float) $this->longitude) === 0.0 &&
+ $this->timezone === '';
+ }
}
diff --git a/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php b/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php
new file mode 100644
index 00000000..4a00d5b9
--- /dev/null
+++ b/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php
@@ -0,0 +1,95 @@
+assertTrue($e->isNonLocatableAddress());
+ $this->assertEquals('Ignored visit with no IP address', $e->getMessage());
+ $this->assertEquals(0, $e->getCode());
+ $this->assertNull($e->getPrevious());
+ }
+
+ /** @test */
+ public function forLocalhostInitializesException(): void
+ {
+ $e = IpCannotBeLocatedException::forLocalhost();
+
+ $this->assertTrue($e->isNonLocatableAddress());
+ $this->assertEquals('Ignored localhost address', $e->getMessage());
+ $this->assertEquals(0, $e->getCode());
+ $this->assertNull($e->getPrevious());
+ }
+
+ /**
+ * @test
+ * @dataProvider provideErrors
+ */
+ public function forErrorInitializesException(Throwable $prev): void
+ {
+ $e = IpCannotBeLocatedException::forError($prev);
+
+ $this->assertFalse($e->isNonLocatableAddress());
+ $this->assertEquals('An error occurred while locating IP', $e->getMessage());
+ $this->assertEquals($prev->getCode(), $e->getCode());
+ $this->assertSame($prev, $e->getPrevious());
+ }
+
+ public function provideErrors(): iterable
+ {
+ yield 'Simple exception with positive code' => [new Exception('Some message', 100)];
+ yield 'Runtime exception with negative code' => [new RuntimeException('Something went wrong', -50)];
+ yield 'Logic exception with default code' => [new LogicException('Conditions unmet')];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideConstructorArgs
+ */
+ public function constructorInitializesException(): void
+ {
+ $args = func_get_args();
+ [$isNonLocatableAddress, $message] = $args;
+ $code = $args[2] ?? 0;
+ $prev = $args[3] ?? null;
+
+ switch (count($args)) {
+ case 2:
+ $e = new IpCannotBeLocatedException($isNonLocatableAddress, $message);
+ break;
+ case 3:
+ $e = new IpCannotBeLocatedException($isNonLocatableAddress, $message, $code);
+ break;
+ default:
+ $e = new IpCannotBeLocatedException($isNonLocatableAddress, $message, $code, $prev);
+ }
+
+ $this->assertEquals($isNonLocatableAddress, $e->isNonLocatableAddress());
+ $this->assertEquals($message, $e->getMessage());
+ $this->assertEquals($code, $e->getCode());
+ $this->assertEquals($prev, $e->getPrevious());
+ }
+
+ public function provideConstructorArgs(): iterable
+ {
+ yield 'without default args' => [true, 'Message'];
+ yield 'without prev' => [true, 'Message', random_int(1, 100)];
+ yield 'without all args' => [false, 'Foo', random_int(1, 100), new RuntimeException('Foo')];
+ }
+}
diff --git a/module/Core/test/Service/VisitServiceTest.php b/module/Core/test/Service/VisitServiceTest.php
index 46f1da37..a2821fa6 100644
--- a/module/Core/test/Service/VisitServiceTest.php
+++ b/module/Core/test/Service/VisitServiceTest.php
@@ -70,8 +70,11 @@ class VisitServiceTest extends TestCase
$clear->shouldHaveBeenCalledTimes(floor(count($unlocatedVisits) / 200) + 1);
}
- /** @test */
- public function visitsWhichCannotBeLocatedAreIgnored(): void
+ /**
+ * @test
+ * @dataProvider provideIsNonLocatableAddress
+ */
+ public function visitsWhichCannotBeLocatedAreIgnoredOrLocatedAsEmpty(bool $isNonLocatableAddress): void
{
$unlocatedVisits = [
new Visit(new ShortUrl('foo'), Visitor::emptyInstance()),
@@ -88,14 +91,20 @@ class VisitServiceTest extends TestCase
$clear = $this->em->clear()->will(function () {
});
- $this->visitService->locateUnlocatedVisits(function () {
- throw new IpCannotBeLocatedException(false, 'Cannot be located');
+ $this->visitService->locateUnlocatedVisits(function () use ($isNonLocatableAddress) {
+ throw new IpCannotBeLocatedException($isNonLocatableAddress, 'Cannot be located');
});
$findUnlocatedVisits->shouldHaveBeenCalledOnce();
$getRepo->shouldHaveBeenCalledOnce();
- $persist->shouldNotHaveBeenCalled();
+ $persist->shouldHaveBeenCalledTimes($isNonLocatableAddress ? 1 : 0);
$flush->shouldHaveBeenCalledOnce();
$clear->shouldHaveBeenCalledOnce();
}
+
+ public function provideIsNonLocatableAddress(): iterable
+ {
+ yield 'The address is locatable' => [false];
+ yield 'The address is non-locatable' => [true];
+ }
}