'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> */ private array $records = []; /** @var array */ private array $index = []; /** @var array */ public array $removedFromIndex = []; public ?int $lastStoredTtl = null; /** * @return array */ protected function readIndexMembers(): array { return $this->index; } /** * @return array|null */ protected function readRecord(string $visitorKey): ?array { return $this->records[$visitorKey] ?? null; } /** * @param array $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 $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; } }