50daf92d6d
Task 4 плана 2026-07-13-visitors-analytics.md: VisitTracker (создание/обновление
гостя, запись событий, привязка к клиенту) + публичный TrackController@store,
маршрут POST /api/track (throttle:track 60/мин), лимитер в AppServiceProvider,
CSRF-исключение api/track в bootstrap/app.php.
Отклонения от плана (тест-харнесс, не прод-код):
- getCookie('lid_vid', false) в тесте убрал decrypt=false — с withCookie() это
давало двойное шифрование (withCookie сам шифрует plain-значение).
- добавлен withCredentials() перед withCookie()+postJson/getJson — Laravel
тест-клиент по умолчанию не шлёт cookie на JSON-запросы (как XHR
credentials:'omit'); реальный маячок шлёт fetch с credentials:'include',
так что прод не затронут.
Pest: 5/5 (TrackEndpointTest). Pint: app/Services/Tracking + TrackController.
75 lines
3.6 KiB
PHP
75 lines
3.6 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
uses(SharesSupplierPdo::class);
|
||
|
||
test('первое событие view создаёт гостя и ставит куку', function () {
|
||
$r = $this->postJson('/api/track', [
|
||
'event' => 'view',
|
||
'host' => 'liderra.ru',
|
||
'path' => '/',
|
||
'referrer' => 'https://krasnoyarsk.hh.ru/vacancy/1',
|
||
], ['User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X)']);
|
||
|
||
$r->assertStatus(204);
|
||
$r->assertCookie('lid_vid');
|
||
|
||
$v = DB::table('site_visitors')->first();
|
||
expect($v->channel)->toBe('hh');
|
||
expect($v->device)->toBe('phone');
|
||
expect($v->is_human)->toBeFalse(); // пока только браузер, не человек
|
||
expect(DB::table('site_events')->where('event', 'view')->count())->toBe(1);
|
||
});
|
||
|
||
test('utm-метка из ссылки определяет канал', function () {
|
||
$this->postJson('/api/track', [
|
||
'event' => 'view', 'host' => 'liderra.ru', 'path' => '/',
|
||
'utm' => ['utm_source' => 'sms', 'utm_campaign' => 'rassylka-13-07'],
|
||
])->assertStatus(204);
|
||
|
||
$v = DB::table('site_visitors')->first();
|
||
expect($v->channel)->toBe('sms');
|
||
expect($v->utm_campaign)->toBe('rassylka-13-07');
|
||
});
|
||
|
||
test('повторный view с той же кукой не создаёт второго гостя', function () {
|
||
$first = $this->postJson('/api/track', ['event' => 'view', 'host' => 'liderra.ru', 'path' => '/']);
|
||
// getCookie() без второго аргумента — decrypt=true (по умолчанию), возвращает
|
||
// ПЛОСКОЕ значение (снят слой encrypt+CookieValuePrefix). withCookie() ниже сам
|
||
// шифрует заново — если взять уже-зашифрованное значение (decrypt=false), получится
|
||
// двойное шифрование и сервер cookie не распознает (заведёт нового гостя).
|
||
$vid = $first->getCookie('lid_vid')->getValue();
|
||
|
||
// withCredentials(): postJson/getJson по умолчанию НЕ прикладывают cookie
|
||
// (как XHR с credentials:'omit') — withCookie() без него молча теряется.
|
||
// В браузере маячок шлёт fetch с credentials:'include', поэтому куку видит всегда.
|
||
$this->withCredentials()->withCookie('lid_vid', $vid)
|
||
->postJson('/api/track', ['event' => 'view', 'host' => 'lk.liderra.ru', 'path' => '/login'])
|
||
->assertStatus(204);
|
||
|
||
expect(DB::table('site_visitors')->count())->toBe(1);
|
||
expect(DB::table('site_events')->count())->toBe(2);
|
||
});
|
||
|
||
test('событие alive помечает гостя как живого человека', function () {
|
||
$first = $this->postJson('/api/track', ['event' => 'view', 'host' => 'liderra.ru', 'path' => '/']);
|
||
$vid = $first->getCookie('lid_vid')->getValue();
|
||
|
||
$this->withCredentials()->withCookie('lid_vid', $vid)
|
||
->postJson('/api/track', ['event' => 'alive', 'host' => 'liderra.ru', 'path' => '/'])
|
||
->assertStatus(204);
|
||
|
||
expect((bool) DB::table('site_visitors')->first()->is_human)->toBeTrue();
|
||
});
|
||
|
||
test('неизвестное событие отклоняется', function () {
|
||
$this->postJson('/api/track', ['event' => 'drop_table', 'host' => 'liderra.ru'])
|
||
->assertStatus(422);
|
||
expect(DB::table('site_events')->count())->toBe(0);
|
||
});
|