feat(visitors): определение города и оператора по офлайн-базе + признак VPN/хостинга

Гео читается из локального файла (DB-IP Lite) — IP посетителя наружу не уходит.
Нет файла базы → город не определяется, учёт продолжает работать (fail-open).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-13 19:32:34 +03:00
parent dd5fb58990
commit 42adebcc5b
5 changed files with 352 additions and 1 deletions
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use App\Services\Tracking\GeoResolver;
// GeoResolver читает пути к базам через config() → нужен поднятый контейнер приложения.
// Юнит-набор его не поднимает, поэтому явно подключаем TestCase (БД не трогаем).
uses(Tests\TestCase::class);
test('без гео-базы возвращает пустой результат и не падает', function () {
config()->set('services.geoip.city_db', '/nope/city.mmdb');
config()->set('services.geoip.asn_db', '/nope/asn.mmdb');
$res = (new GeoResolver())->lookup('8.8.8.8');
expect($res)->toMatchArray([
'city' => null, 'region' => null, 'asn_org' => null, 'is_datacenter' => false,
]);
});
test('организация-хостинг помечается как дата-центр/VPN', function () {
expect(GeoResolver::looksLikeDatacenter('Tencent Cloud Computing'))->toBeTrue();
expect(GeoResolver::looksLikeDatacenter('DigitalOcean, LLC'))->toBeTrue();
expect(GeoResolver::looksLikeDatacenter('NordVPN'))->toBeTrue();
expect(GeoResolver::looksLikeDatacenter('Tele2 Russia'))->toBeFalse();
expect(GeoResolver::looksLikeDatacenter('Rostelecom'))->toBeFalse();
expect(GeoResolver::looksLikeDatacenter(null))->toBeFalse();
});