222 lines
7.3 KiB
PHP
222 lines
7.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Auth;
|
|
|
|
use App\Auth\AuthService;
|
|
use App\Auth\LoginAttemptRepositoryInterface;
|
|
use App\Shared\Http\SessionManagerInterface;
|
|
use App\User\UserRepositoryInterface;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Tests unitaires pour la protection anti-brute force de AuthService.
|
|
*
|
|
* Vérifie le comportement de checkRateLimit(), recordFailure() et
|
|
* resetRateLimit(). Les constantes testées correspondent aux valeurs
|
|
* définies dans AuthService :
|
|
* - MAX_ATTEMPTS = 5 : nombre d'échecs avant verrouillage
|
|
* - LOCK_MINUTES = 15 : durée du verrouillage en minutes
|
|
*/
|
|
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
|
final class AuthServiceRateLimitTest extends TestCase
|
|
{
|
|
/** @var UserRepositoryInterface&MockObject */
|
|
private UserRepositoryInterface $userRepository;
|
|
|
|
/** @var SessionManagerInterface&MockObject */
|
|
private SessionManagerInterface $sessionManager;
|
|
|
|
/** @var LoginAttemptRepositoryInterface&MockObject */
|
|
private LoginAttemptRepositoryInterface $loginAttemptRepository;
|
|
|
|
private AuthService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
|
|
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
|
|
$this->loginAttemptRepository = $this->createMock(LoginAttemptRepositoryInterface::class);
|
|
|
|
$this->service = new AuthService(
|
|
$this->userRepository,
|
|
$this->sessionManager,
|
|
$this->loginAttemptRepository,
|
|
);
|
|
}
|
|
|
|
|
|
// ── checkRateLimit ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* checkRateLimit() doit retourner 0 si l'IP n'a aucune entrée en base.
|
|
*/
|
|
public function testCheckRateLimitUnknownIpReturnsZero(): void
|
|
{
|
|
$this->loginAttemptRepository->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn(null);
|
|
|
|
$result = $this->service->checkRateLimit('192.168.1.1');
|
|
|
|
$this->assertSame(0, $result);
|
|
}
|
|
|
|
/**
|
|
* checkRateLimit() doit retourner 0 si l'IP a des tentatives
|
|
* mais n'est pas encore verrouillée (locked_until = null).
|
|
*/
|
|
public function testCheckRateLimitNoLockReturnsZero(): void
|
|
{
|
|
$this->loginAttemptRepository->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn([
|
|
'ip' => '192.168.1.1',
|
|
'attempts' => 3,
|
|
'locked_until' => null,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
$result = $this->service->checkRateLimit('192.168.1.1');
|
|
|
|
$this->assertSame(0, $result);
|
|
}
|
|
|
|
/**
|
|
* checkRateLimit() doit retourner le nombre de minutes restantes
|
|
* si l'IP est actuellement verrouillée.
|
|
*/
|
|
public function testCheckRateLimitLockedIpReturnsRemainingMinutes(): void
|
|
{
|
|
$lockedUntil = date('Y-m-d H:i:s', time() + 10 * 60);
|
|
|
|
$this->loginAttemptRepository->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn([
|
|
'ip' => '192.168.1.1',
|
|
'attempts' => 5,
|
|
'locked_until' => $lockedUntil,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
$result = $this->service->checkRateLimit('192.168.1.1');
|
|
|
|
$this->assertGreaterThan(0, $result);
|
|
$this->assertLessThanOrEqual(15, $result);
|
|
}
|
|
|
|
/**
|
|
* checkRateLimit() doit retourner au minimum 1 minute
|
|
* même si le verrouillage expire dans quelques secondes.
|
|
*/
|
|
public function testCheckRateLimitReturnsAtLeastOneMinute(): void
|
|
{
|
|
$lockedUntil = date('Y-m-d H:i:s', time() + 30);
|
|
|
|
$this->loginAttemptRepository->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn([
|
|
'ip' => '192.168.1.1',
|
|
'attempts' => 5,
|
|
'locked_until' => $lockedUntil,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
$result = $this->service->checkRateLimit('192.168.1.1');
|
|
|
|
$this->assertSame(1, $result);
|
|
}
|
|
|
|
/**
|
|
* checkRateLimit() doit retourner 0 si le verrouillage est expiré.
|
|
*/
|
|
public function testCheckRateLimitExpiredLockReturnsZero(): void
|
|
{
|
|
$lockedUntil = date('Y-m-d H:i:s', time() - 60);
|
|
|
|
$this->loginAttemptRepository->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn([
|
|
'ip' => '192.168.1.1',
|
|
'attempts' => 5,
|
|
'locked_until' => $lockedUntil,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
$result = $this->service->checkRateLimit('192.168.1.1');
|
|
|
|
$this->assertSame(0, $result);
|
|
}
|
|
|
|
/**
|
|
* checkRateLimit() doit toujours appeler deleteExpired() avant la vérification.
|
|
*/
|
|
public function testCheckRateLimitCallsDeleteExpired(): void
|
|
{
|
|
$this->loginAttemptRepository
|
|
->expects($this->once())
|
|
->method('deleteExpired');
|
|
|
|
$this->loginAttemptRepository->method('findByIp')->willReturn(null);
|
|
|
|
$this->service->checkRateLimit('192.168.1.1');
|
|
}
|
|
|
|
|
|
// ── recordFailure ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* recordFailure() doit déléguer avec MAX_ATTEMPTS = 5 et LOCK_MINUTES = 15.
|
|
*/
|
|
public function testRecordFailureDelegatesWithConstants(): void
|
|
{
|
|
$this->loginAttemptRepository
|
|
->expects($this->once())
|
|
->method('recordFailure')
|
|
->with('192.168.1.1', 5, 15);
|
|
|
|
$this->service->recordFailure('192.168.1.1');
|
|
}
|
|
|
|
/**
|
|
* recordFailure() doit transmettre l'adresse IP exacte au dépôt.
|
|
*/
|
|
public function testRecordFailurePassesIpAddress(): void
|
|
{
|
|
$ip = '10.0.0.42';
|
|
|
|
$this->loginAttemptRepository
|
|
->expects($this->once())
|
|
->method('recordFailure')
|
|
->with($ip, $this->anything(), $this->anything());
|
|
|
|
$this->service->recordFailure($ip);
|
|
}
|
|
|
|
|
|
// ── resetRateLimit ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* resetRateLimit() doit appeler resetForIp() avec l'adresse IP fournie.
|
|
*/
|
|
public function testResetRateLimitCallsResetForIp(): void
|
|
{
|
|
$ip = '192.168.1.1';
|
|
|
|
$this->loginAttemptRepository
|
|
->expects($this->once())
|
|
->method('resetForIp')
|
|
->with($ip);
|
|
|
|
$this->service->resetRateLimit($ip);
|
|
}
|
|
|
|
/**
|
|
* resetRateLimit() ne doit pas appeler recordFailure() ni deleteExpired().
|
|
*/
|
|
public function testResetRateLimitCallsNothingElse(): void
|
|
{
|
|
$this->loginAttemptRepository->expects($this->never())->method('recordFailure');
|
|
$this->loginAttemptRepository->expects($this->never())->method('deleteExpired');
|
|
$this->loginAttemptRepository->method('resetForIp');
|
|
|
|
$this->service->resetRateLimit('192.168.1.1');
|
|
}
|
|
}
|