throttle(); for ($i = 0; $i < 4; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertFalse($t->isLocked('admin', '10.0.0.1')); } public function testFifthFailureLocksForOneMinute(): void { $t = $this->throttle(); for ($i = 0; $i < 5; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertTrue($t->isLocked('admin', '10.0.0.1')); self::assertGreaterThanOrEqual(60, $t->lockoutSecondsRemaining('admin', '10.0.0.1')); self::assertLessThanOrEqual(60, $t->lockoutSecondsRemaining('admin', '10.0.0.1')); } public function testTenthFailureLocksForFiveMinutes(): void { $now = 1000000; $t = $this->throttle($now); for ($i = 0; $i < 10; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertSame(300, $t->lockoutSecondsRemaining('admin', '10.0.0.1')); } public function testFifteenthFailureLocksForThirtyMinutes(): void { $now = 1000000; $t = $this->throttle($now); for ($i = 0; $i < 15; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertSame(1800, $t->lockoutSecondsRemaining('admin', '10.0.0.1')); } public function testLockoutExpiresAfterTimeAdvance(): void { $now = 1000000; $t = new LoginThrottle($this->logger(), function () use (&$now): int { return $now; }); for ($i = 0; $i < 5; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertTrue($t->isLocked('admin', '10.0.0.1')); $now += 61; self::assertFalse($t->isLocked('admin', '10.0.0.1')); } public function testDifferentIpHasIndependentBucket(): void { $t = $this->throttle(); for ($i = 0; $i < 5; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } self::assertTrue($t->isLocked('admin', '10.0.0.1')); self::assertFalse($t->isLocked('admin', '10.0.0.2')); } public function testClearResetsBucket(): void { $t = $this->throttle(); for ($i = 0; $i < 5; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } $t->clear('admin', '10.0.0.1'); self::assertFalse($t->isLocked('admin', '10.0.0.1')); self::assertSame(0, $t->lockoutSecondsRemaining('admin', '10.0.0.1')); } public function testUsernameCaseDoesNotMultiplyBuckets(): void { $t = $this->throttle(); for ($i = 0; $i < 3; ++$i) { $t->recordFailure('admin', '10.0.0.1'); } for ($i = 0; $i < 2; ++$i) { $t->recordFailure('ADMIN', '10.0.0.1'); } self::assertTrue($t->isLocked('Admin', '10.0.0.1')); } private function throttle(?int $fixedTime = null): LoginThrottle { if ($fixedTime === null) { return new LoginThrottle($this->logger()); } return new LoginThrottle($this->logger(), static fn (): int => $fixedTime); } private function logger(): LoggerInterface { $l = new Logger('test'); $l->pushHandler(new NullHandler()); return $l; } }