cache = new ArrayAdapter(); $this->rateLimiter = new LoginRateLimiter($this->cache); } #[Test] public function checkReturnsAllowedForFirstAttempt(): void { $request = $this->createRequest('192.168.1.1'); $result = $this->rateLimiter->check($request, 'test@example.com'); self::assertTrue($result->isAllowed); self::assertFalse($result->ipBlocked); self::assertSame(0, $result->attempts); self::assertSame(0, $result->delaySeconds); self::assertFalse($result->requiresCaptcha); } #[Test] public function recordFailureIncrementsAttemptsAndCalculatesFibonacciDelay(): void { $request = $this->createRequest('192.168.1.1'); $email = 'test@example.com'; // First failure - no delay (1 attempt = 0s) $result = $this->rateLimiter->recordFailure($request, $email); self::assertSame(1, $result->attempts); self::assertSame(0, $result->delaySeconds); // Second failure - delay 1s (F0) $result = $this->rateLimiter->recordFailure($request, $email); self::assertSame(2, $result->attempts); self::assertSame(1, $result->delaySeconds); // Third failure - delay 1s (F1) $result = $this->rateLimiter->recordFailure($request, $email); self::assertSame(3, $result->attempts); self::assertSame(1, $result->delaySeconds); // Fourth failure - delay 2s (F2) $result = $this->rateLimiter->recordFailure($request, $email); self::assertSame(4, $result->attempts); self::assertSame(2, $result->delaySeconds); // Fifth failure - delay 3s (F3), CAPTCHA required $result = $this->rateLimiter->recordFailure($request, $email); self::assertSame(5, $result->attempts); self::assertSame(3, $result->delaySeconds); self::assertTrue($result->requiresCaptcha); } #[Test] public function checkReturnsCorrectStateAfterFailures(): void { $request = $this->createRequest('192.168.1.1'); $email = 'test@example.com'; // Record 5 failures for ($i = 0; $i < 5; ++$i) { $this->rateLimiter->recordFailure($request, $email); } // Check should return the current state $result = $this->rateLimiter->check($request, $email); self::assertSame(5, $result->attempts); self::assertTrue($result->requiresCaptcha); } #[Test] public function blockIpPreventsSubsequentAttempts(): void { $ip = '192.168.1.1'; $request = $this->createRequest($ip); $this->rateLimiter->blockIp($ip); $result = $this->rateLimiter->check($request, 'any@email.com'); self::assertTrue($result->ipBlocked); self::assertFalse($result->isAllowed); self::assertGreaterThan(0, $result->retryAfter); } #[Test] public function recordFailureBlocksIpAfter20Attempts(): void { $request = $this->createRequest('192.168.1.1'); $email = 'attacker@example.com'; // Record 19 failures - should not be blocked for ($i = 0; $i < 19; ++$i) { $result = $this->rateLimiter->recordFailure($request, $email); self::assertFalse($result->ipBlocked); } // 20th failure - should be blocked $result = $this->rateLimiter->recordFailure($request, $email); self::assertTrue($result->ipBlocked); self::assertSame(LoginRateLimiterInterface::IP_BLOCK_DURATION, $result->retryAfter); } #[Test] public function resetClearsAttemptsForEmail(): void { $request = $this->createRequest('192.168.1.1'); $email = 'test@example.com'; // Record some failures $this->rateLimiter->recordFailure($request, $email); $this->rateLimiter->recordFailure($request, $email); // Reset $this->rateLimiter->reset($email); // Check should show 0 attempts $result = $this->rateLimiter->check($request, $email); self::assertSame(0, $result->attempts); } #[Test] public function isIpBlockedReturnsFalseForUnblockedIp(): void { self::assertFalse($this->rateLimiter->isIpBlocked('192.168.1.1')); } #[Test] public function isIpBlockedReturnsTrueForBlockedIp(): void { $ip = '192.168.1.1'; $this->rateLimiter->blockIp($ip); self::assertTrue($this->rateLimiter->isIpBlocked($ip)); } #[Test] public function differentEmailsHaveSeparateAttemptCounters(): void { $request = $this->createRequest('192.168.1.1'); // Record failures for email1 $this->rateLimiter->recordFailure($request, 'email1@test.com'); $this->rateLimiter->recordFailure($request, 'email1@test.com'); // Record failure for email2 $this->rateLimiter->recordFailure($request, 'email2@test.com'); // Check each email $result1 = $this->rateLimiter->check($request, 'email1@test.com'); $result2 = $this->rateLimiter->check($request, 'email2@test.com'); self::assertSame(2, $result1->attempts); self::assertSame(1, $result2->attempts); } #[Test] public function emailNormalizationIsCaseInsensitive(): void { $request = $this->createRequest('192.168.1.1'); $this->rateLimiter->recordFailure($request, 'Test@Example.COM'); $this->rateLimiter->recordFailure($request, 'test@example.com'); $result = $this->rateLimiter->check($request, 'TEST@EXAMPLE.COM'); self::assertSame(2, $result->attempts); } private function createRequest(string $clientIp): Request { $request = Request::create('/api/login', 'POST'); $request->server->set('REMOTE_ADDR', $clientIp); return $request; } }