mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 12:13:13 +08:00
Fix GeolocationDbUpdater test
This commit is contained in:
@@ -6,14 +6,16 @@ namespace ShlinkioTest\Shlink\Core\Geolocation;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Closure;
|
||||
use GeoIp2\Database\Reader;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
use Shlinkio\Shlink\Core\Config\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\Core\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\Core\Geolocation\Entity\GeolocationDbUpdate;
|
||||
use Shlinkio\Shlink\Core\Geolocation\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Core\Geolocation\GeolocationDownloadProgressHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Geolocation\GeolocationResult;
|
||||
@@ -29,17 +31,24 @@ use function range;
|
||||
class GeolocationDbUpdaterTest extends TestCase
|
||||
{
|
||||
private MockObject & DbUpdaterInterface $dbUpdater;
|
||||
private MockObject & Reader $geoLiteDbReader;
|
||||
private MockObject & Lock\LockInterface $lock;
|
||||
private MockObject & EntityManagerInterface $em;
|
||||
/** @var MockObject&EntityRepository<GeolocationDbUpdate> */
|
||||
private MockObject & EntityRepository $repo;
|
||||
/** @var GeolocationDownloadProgressHandlerInterface&object{beforeDownloadCalled: bool, handleProgressCalled: bool} */
|
||||
private GeolocationDownloadProgressHandlerInterface $progressHandler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->createMock(DbUpdaterInterface::class);
|
||||
$this->geoLiteDbReader = $this->createMock(Reader::class);
|
||||
|
||||
$this->lock = $this->createMock(Lock\SharedLockInterface::class);
|
||||
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
|
||||
|
||||
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||
$this->repo = $this->createMock(EntityRepository::class);
|
||||
$this->em->method('getRepository')->willReturn($this->repo);
|
||||
|
||||
$this->progressHandler = new class implements GeolocationDownloadProgressHandlerInterface {
|
||||
public function __construct(
|
||||
public bool $beforeDownloadCalled = false,
|
||||
@@ -59,6 +68,32 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function properResultIsReturnedIfMostRecentUpdateIsInProgress(): void
|
||||
{
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn([GeolocationDbUpdate::withReason('')]);
|
||||
$this->dbUpdater->expects($this->never())->method('databaseFileExists');
|
||||
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::UPDATE_IN_PROGRESS, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function properResultIsReturnedIfMaxConsecutiveErrorsAreReached(): void
|
||||
{
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn([
|
||||
GeolocationDbUpdate::withReason('')->finishWithError(''),
|
||||
GeolocationDbUpdate::withReason('')->finishWithError(''),
|
||||
GeolocationDbUpdate::withReason('')->finishWithError(''),
|
||||
]);
|
||||
$this->dbUpdater->expects($this->never())->method('databaseFileExists');
|
||||
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::MAX_ERRORS_REACHED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function properResultIsReturnedWhenLicenseIsMissing(): void
|
||||
{
|
||||
@@ -66,7 +101,9 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->willThrowException(
|
||||
new MissingLicenseException(''),
|
||||
);
|
||||
$this->geoLiteDbReader->expects($this->never())->method('metadata');
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn([
|
||||
GeolocationDbUpdate::withReason('')->finishSuccessfully(),
|
||||
]);
|
||||
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate($this->progressHandler);
|
||||
|
||||
@@ -74,16 +111,19 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
self::assertEquals(GeolocationResult::LICENSE_MISSING, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||
#[Test, DataProvider('provideDbDoesNotExist')]
|
||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(Closure $setUp): void
|
||||
{
|
||||
$prev = new DbUpdateException('');
|
||||
|
||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(false);
|
||||
$expectedReason = $setUp($this);
|
||||
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->with(
|
||||
$this->isInstanceOf(Closure::class),
|
||||
)->willThrowException($prev);
|
||||
$this->geoLiteDbReader->expects($this->never())->method('metadata');
|
||||
$this->em->expects($this->once())->method('persist')->with($this->callback(
|
||||
fn (GeolocationDbUpdate $newUpdate): bool => $newUpdate->reason === $expectedReason,
|
||||
));
|
||||
|
||||
try {
|
||||
$this->geolocationDbUpdater()->checkDbUpdate($this->progressHandler);
|
||||
@@ -96,17 +136,31 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideDbDoesNotExist(): iterable
|
||||
{
|
||||
yield 'file does not exist' => [function (self $test): string {
|
||||
$test->repo->expects($test->once())->method('findBy')->willReturn([
|
||||
GeolocationDbUpdate::withReason('')->finishSuccessfully(),
|
||||
]);
|
||||
$test->dbUpdater->expects($test->once())->method('databaseFileExists')->willReturn(false);
|
||||
return 'Geolocation db file does not exist';
|
||||
}];
|
||||
yield 'no attempts' => [function (self $test): string {
|
||||
$test->repo->expects($test->once())->method('findBy')->willReturn([]);
|
||||
$test->dbUpdater->expects($test->never())->method('databaseFileExists');
|
||||
return 'No download attempts tracked for this instance';
|
||||
}];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideBigDays')]
|
||||
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
||||
public function exceptionIsThrownWhenOlderDbIsOldEnoughAndDownloadFails(int $days): void
|
||||
{
|
||||
$prev = new DbUpdateException('');
|
||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->with(
|
||||
$this->isInstanceOf(Closure::class),
|
||||
)->willThrowException($prev);
|
||||
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
|
||||
$this->buildMetaWithBuildEpoch(Chronos::now()->subDays($days)->getTimestamp()),
|
||||
);
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn([self::createFinishedOldUpdate($days)]);
|
||||
|
||||
try {
|
||||
$this->geolocationDbUpdater()->checkDbUpdate();
|
||||
@@ -120,74 +174,109 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
|
||||
public static function provideBigDays(): iterable
|
||||
{
|
||||
yield [36];
|
||||
yield [31];
|
||||
yield [50];
|
||||
yield [75];
|
||||
yield [100];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideSmallDays')]
|
||||
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
|
||||
#[Test]
|
||||
public function exceptionIsThrownWhenUnknownErrorHappens(): void
|
||||
{
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->with(
|
||||
$this->isInstanceOf(Closure::class),
|
||||
)->willThrowException(new RuntimeException('An error occurred'));
|
||||
|
||||
$newUpdate = null;
|
||||
$this->em->expects($this->once())->method('persist')->with($this->callback(
|
||||
function (GeolocationDbUpdate $u) use (&$newUpdate): bool {
|
||||
$newUpdate = $u;
|
||||
return true;
|
||||
},
|
||||
));
|
||||
|
||||
try {
|
||||
$this->geolocationDbUpdater()->checkDbUpdate($this->progressHandler);
|
||||
self::fail();
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
self::assertTrue($this->progressHandler->beforeDownloadCalled);
|
||||
self::assertNotNull($newUpdate);
|
||||
self::assertTrue($newUpdate->isError());
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideNotAldEnoughDays')]
|
||||
public function databaseIsNotUpdatedIfItIsNewEnough(int $days): void
|
||||
{
|
||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
||||
$this->dbUpdater->expects($this->never())->method('downloadFreshCopy');
|
||||
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
|
||||
$this->buildMetaWithBuildEpoch($buildEpoch),
|
||||
);
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn([self::createFinishedOldUpdate($days)]);
|
||||
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
|
||||
}
|
||||
|
||||
public static function provideSmallDays(): iterable
|
||||
public static function provideNotAldEnoughDays(): iterable
|
||||
{
|
||||
$generateParamsWithTimestamp = static function (int $days) {
|
||||
$timestamp = Chronos::now()->subDays($days)->getTimestamp();
|
||||
return [$days % 2 === 0 ? $timestamp : (string) $timestamp];
|
||||
};
|
||||
|
||||
return array_map($generateParamsWithTimestamp, range(0, 34));
|
||||
return array_map(static fn (int $value) => [$value], range(0, 29));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function exceptionIsThrownWhenCheckingExistingDatabaseWithInvalidBuildEpoch(): void
|
||||
{
|
||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
||||
$this->dbUpdater->expects($this->never())->method('downloadFreshCopy');
|
||||
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
|
||||
$this->buildMetaWithBuildEpoch('invalid'),
|
||||
);
|
||||
#[Test, DataProvider('provideUpdatesThatWillDownload')]
|
||||
public function properResultIsReturnedWhenDownloadSucceeds(
|
||||
array $updates,
|
||||
GeolocationResult $expectedResult,
|
||||
string $expectedReason,
|
||||
): void {
|
||||
$this->repo->expects($this->once())->method('findBy')->willReturn($updates);
|
||||
$this->dbUpdater->method('databaseFileExists')->willReturn(true);
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy');
|
||||
$this->em->expects($this->once())->method('persist')->with($this->callback(
|
||||
fn (GeolocationDbUpdate $newUpdate): bool => $newUpdate->reason === $expectedReason,
|
||||
));
|
||||
|
||||
$this->expectException(GeolocationDbUpdateFailedException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Build epoch with value "invalid" from existing geolocation database, could not be parsed to integer.',
|
||||
);
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||
|
||||
$this->geolocationDbUpdater()->checkDbUpdate();
|
||||
self::assertEquals($expectedResult, $result);
|
||||
}
|
||||
|
||||
private function buildMetaWithBuildEpoch(string|int $buildEpoch): Metadata
|
||||
public static function provideUpdatesThatWillDownload(): iterable
|
||||
{
|
||||
return new Metadata([
|
||||
'binary_format_major_version' => '',
|
||||
'binary_format_minor_version' => '',
|
||||
'build_epoch' => $buildEpoch,
|
||||
'database_type' => '',
|
||||
'languages' => '',
|
||||
'description' => '',
|
||||
'ip_version' => '',
|
||||
'node_count' => 1,
|
||||
'record_size' => 4,
|
||||
]);
|
||||
yield 'no updates' => [[], GeolocationResult::DB_CREATED, 'No download attempts tracked for this instance'];
|
||||
yield 'old successful update' => [
|
||||
[self::createFinishedOldUpdate(days: 31)],
|
||||
GeolocationResult::DB_UPDATED,
|
||||
'Last successful attempt is old enough',
|
||||
];
|
||||
yield 'not enough errors' => [
|
||||
[self::createFinishedOldUpdate(days: 3, successful: false)],
|
||||
GeolocationResult::DB_UPDATED,
|
||||
'Max consecutive errors not reached',
|
||||
];
|
||||
}
|
||||
|
||||
public static function createFinishedOldUpdate(int $days, bool $successful = true): GeolocationDbUpdate
|
||||
{
|
||||
Chronos::setTestNow(Chronos::now()->subDays($days));
|
||||
$update = GeolocationDbUpdate::withReason('');
|
||||
if ($successful) {
|
||||
$update->finishSuccessfully();
|
||||
} else {
|
||||
$update->finishWithError('');
|
||||
}
|
||||
Chronos::setTestNow();
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTrackingOptions')]
|
||||
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
||||
{
|
||||
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
||||
$this->dbUpdater->expects($this->never())->method('databaseFileExists');
|
||||
$this->geoLiteDbReader->expects($this->never())->method('metadata');
|
||||
$this->em->expects($this->never())->method('getRepository');
|
||||
|
||||
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
|
||||
}
|
||||
@@ -204,11 +293,6 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
$locker = $this->createMock(Lock\LockFactory::class);
|
||||
$locker->method('createLock')->with($this->isType('string'))->willReturn($this->lock);
|
||||
|
||||
return new GeolocationDbUpdater(
|
||||
$this->dbUpdater,
|
||||
fn () => $this->geoLiteDbReader,
|
||||
$locker,
|
||||
$options ?? new TrackingOptions(),
|
||||
);
|
||||
return new GeolocationDbUpdater($this->dbUpdater, $locker, $options ?? new TrackingOptions(), $this->em, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user