167 lines
5.3 KiB
PHP
167 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Traffic;
|
|
|
|
use App\Models\User;
|
|
use App\Services\Traffic\BotClassifier;
|
|
use App\Services\Traffic\OnlineVisitorRepository;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Request;
|
|
use Tests\TestCase;
|
|
|
|
final class OnlineVisitorTrackingTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_googlebot_is_classified_as_search_bot(): void
|
|
{
|
|
$classifier = app(BotClassifier::class);
|
|
$request = Request::create('/wallpapers', 'GET', server: ['HTTP_USER_AGENT' => 'Googlebot/2.1']);
|
|
|
|
$result = $classifier->classify($request);
|
|
|
|
self::assertTrue($result['is_bot']);
|
|
self::assertSame('search_bot', $result['type']);
|
|
self::assertSame('Googlebot', $result['family']);
|
|
}
|
|
|
|
public function test_gptbot_is_classified_as_ai_bot(): void
|
|
{
|
|
$classifier = app(BotClassifier::class);
|
|
$request = Request::create('/art/1/test', 'GET', server: ['HTTP_USER_AGENT' => 'GPTBot']);
|
|
|
|
$result = $classifier->classify($request);
|
|
|
|
self::assertTrue($result['is_bot']);
|
|
self::assertSame('ai_bot', $result['type']);
|
|
self::assertSame('GPTBot', $result['family']);
|
|
}
|
|
|
|
public function test_suspicious_user_agent_is_classified_as_suspicious_bot(): void
|
|
{
|
|
$classifier = app(BotClassifier::class);
|
|
$request = Request::create('/rate.php', 'GET', server: ['HTTP_USER_AGENT' => 'python-requests/2.31']);
|
|
|
|
$result = $classifier->classify($request);
|
|
|
|
self::assertTrue($result['is_bot']);
|
|
self::assertSame('suspicious_bot', $result['type']);
|
|
self::assertSame('python-requests', $result['family']);
|
|
}
|
|
|
|
public function test_logged_in_user_is_tracked_with_ttl_and_hit_counter(): void
|
|
{
|
|
$user = User::factory()->create(['role' => 'admin', 'name' => 'Gregor']);
|
|
$repository = new InMemoryOnlineVisitorRepository(app(BotClassifier::class));
|
|
|
|
$request = Request::create('/wallpapers', 'GET', server: [
|
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0',
|
|
'HTTP_CF_CONNECTING_IP' => '188.230.12.14',
|
|
]);
|
|
$request->setUserResolver(static fn (): User => $user);
|
|
|
|
$repository->track($request);
|
|
$repository->track($request);
|
|
|
|
$records = $repository->all();
|
|
|
|
self::assertCount(1, $records);
|
|
self::assertSame('human_logged', $records[0]['type']);
|
|
self::assertSame(2, $records[0]['hits']);
|
|
self::assertSame('188.230.xxx.xxx', $records[0]['ip_masked']);
|
|
self::assertSame(OnlineVisitorRepository::TTL_SECONDS, $repository->lastStoredTtl);
|
|
}
|
|
|
|
public function test_guest_tracking_cleans_expired_records_from_index(): void
|
|
{
|
|
$repository = new InMemoryOnlineVisitorRepository(app(BotClassifier::class));
|
|
|
|
$request = Request::create('/news/test', 'GET', server: [
|
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) Safari/605.1.15',
|
|
'REMOTE_ADDR' => '192.168.10.22',
|
|
]);
|
|
$request->cookies->set((string) config('session.cookie'), 'guest-session-cookie');
|
|
|
|
$repository->track($request);
|
|
$repository->seedIndexOnly('guest:expired');
|
|
|
|
$records = $repository->all();
|
|
$summary = $repository->summary();
|
|
$pages = $repository->activePages();
|
|
|
|
self::assertCount(1, $records);
|
|
self::assertSame('human_guest', $records[0]['type']);
|
|
self::assertSame(1, $summary['guests']);
|
|
self::assertSame('/news/test', $pages[0]['url']);
|
|
self::assertSame(['guest:expired'], $repository->removedFromIndex);
|
|
}
|
|
}
|
|
|
|
final class InMemoryOnlineVisitorRepository extends OnlineVisitorRepository
|
|
{
|
|
/** @var array<string, array<string, mixed>> */
|
|
private array $records = [];
|
|
|
|
/** @var array<int, string> */
|
|
private array $index = [];
|
|
|
|
/** @var array<int, string> */
|
|
public array $removedFromIndex = [];
|
|
|
|
public ?int $lastStoredTtl = null;
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
protected function readIndexMembers(): array
|
|
{
|
|
return $this->index;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
protected function readRecord(string $visitorKey): ?array
|
|
{
|
|
return $this->records[$visitorKey] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $record
|
|
*/
|
|
protected function storeRecord(string $visitorKey, array $record, int $ttlSeconds): void
|
|
{
|
|
$this->records[$visitorKey] = $record;
|
|
$this->lastStoredTtl = $ttlSeconds;
|
|
}
|
|
|
|
protected function addIndexMember(string $visitorKey): void
|
|
{
|
|
if (! in_array($visitorKey, $this->index, true)) {
|
|
$this->index[] = $visitorKey;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $visitorKeys
|
|
*/
|
|
protected function removeIndexMembers(array $visitorKeys): void
|
|
{
|
|
foreach ($visitorKeys as $visitorKey) {
|
|
$this->removedFromIndex[] = $visitorKey;
|
|
$this->index = array_values(array_filter($this->index, static fn (string $indexedKey): bool => $indexedKey !== $visitorKey));
|
|
}
|
|
}
|
|
|
|
protected function deleteRecord(string $visitorKey): void
|
|
{
|
|
unset($this->records[$visitorKey]);
|
|
}
|
|
|
|
public function seedIndexOnly(string $visitorKey): void
|
|
{
|
|
$this->index[] = $visitorKey;
|
|
}
|
|
} |