179 lines
6.5 KiB
PHP
179 lines
6.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Auth;
|
|
|
|
use App\Auth\AuthController;
|
|
use App\Auth\AuthServiceInterface;
|
|
use App\Shared\Http\ClientIpResolver;
|
|
use App\Shared\Http\FlashServiceInterface;
|
|
use App\User\User;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Tests\ControllerTestBase;
|
|
|
|
/**
|
|
* Tests unitaires pour AuthController.
|
|
*
|
|
* Couvre showLogin(), login() et logout() sans passer par le routeur Slim.
|
|
* AuthService, FlashService et Twig sont mockés — aucune session PHP,
|
|
* aucune base de données, aucun serveur HTTP n'est requis.
|
|
*/
|
|
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
|
final class AuthControllerTest extends ControllerTestBase
|
|
{
|
|
/** @var \Slim\Views\Twig&MockObject */
|
|
private \Slim\Views\Twig $view;
|
|
|
|
/** @var AuthServiceInterface&MockObject */
|
|
private AuthServiceInterface $authService;
|
|
|
|
/** @var FlashServiceInterface&MockObject */
|
|
private FlashServiceInterface $flash;
|
|
|
|
private AuthController $controller;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->view = $this->makeTwigMock();
|
|
$this->authService = $this->createMock(AuthServiceInterface::class);
|
|
$this->flash = $this->createMock(FlashServiceInterface::class);
|
|
|
|
$this->controller = new AuthController(
|
|
$this->view,
|
|
$this->authService,
|
|
$this->flash,
|
|
new ClientIpResolver(['*']),
|
|
);
|
|
}
|
|
|
|
// ── showLogin ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* showLogin() doit rediriger vers /admin/posts si l'utilisateur est déjà connecté.
|
|
*/
|
|
public function testShowLoginRedirectsWhenAlreadyLoggedIn(): void
|
|
{
|
|
$this->authService->method('isLoggedIn')->willReturn(true);
|
|
|
|
$res = $this->controller->showLogin($this->makeGet('/auth/login'), $this->makeResponse());
|
|
|
|
$this->assertRedirectTo($res, '/admin/posts');
|
|
}
|
|
|
|
/**
|
|
* showLogin() doit rendre le formulaire de connexion si l'utilisateur n'est pas connecté.
|
|
*/
|
|
public function testShowLoginRendersFormWhenNotLoggedIn(): void
|
|
{
|
|
$this->authService->method('isLoggedIn')->willReturn(false);
|
|
|
|
$this->view->expects($this->once())
|
|
->method('render')
|
|
->with($this->anything(), 'pages/auth/login.twig', $this->anything())
|
|
->willReturnArgument(0);
|
|
|
|
$res = $this->controller->showLogin($this->makeGet('/auth/login'), $this->makeResponse());
|
|
|
|
$this->assertStatus($res, 200);
|
|
}
|
|
|
|
// ── login ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* login() doit rediriger avec un message flash si l'IP est verrouillée.
|
|
*/
|
|
public function testLoginRedirectsWhenRateLimited(): void
|
|
{
|
|
$this->authService->method('checkRateLimit')->willReturn(5);
|
|
|
|
$this->flash->expects($this->once())->method('set')
|
|
->with('login_error', $this->stringContains('Trop de tentatives'));
|
|
|
|
$req = $this->makePost('/auth/login', [], ['REMOTE_ADDR' => '10.0.0.1']);
|
|
$res = $this->controller->login($req, $this->makeResponse());
|
|
|
|
$this->assertRedirectTo($res, '/auth/login');
|
|
}
|
|
|
|
/**
|
|
* login() doit conjuguer correctement le singulier/pluriel dans le message de rate limit.
|
|
*/
|
|
public function testLoginRateLimitMessageIsSingularForOneMinute(): void
|
|
{
|
|
$this->authService->method('checkRateLimit')->willReturn(1);
|
|
|
|
$this->flash->expects($this->once())->method('set')
|
|
->with('login_error', $this->logicalNot($this->stringContains('minutes')));
|
|
|
|
$req = $this->makePost('/auth/login', [], ['REMOTE_ADDR' => '10.0.0.1']);
|
|
$this->controller->login($req, $this->makeResponse());
|
|
}
|
|
|
|
/**
|
|
* login() doit enregistrer l'échec et rediriger si les identifiants sont invalides.
|
|
*/
|
|
public function testLoginRecordsFailureOnInvalidCredentials(): void
|
|
{
|
|
$this->authService->method('checkRateLimit')->willReturn(0);
|
|
$this->authService->method('authenticate')->willReturn(null);
|
|
|
|
$this->authService->expects($this->once())->method('recordFailure');
|
|
$this->flash->expects($this->once())->method('set')
|
|
->with('login_error', 'Identifiants invalides');
|
|
|
|
$req = $this->makePost('/auth/login', ['username' => 'alice', 'password' => 'wrong']);
|
|
$res = $this->controller->login($req, $this->makeResponse());
|
|
|
|
$this->assertRedirectTo($res, '/auth/login');
|
|
}
|
|
|
|
/**
|
|
* login() doit ouvrir la session et rediriger vers /admin/posts en cas de succès.
|
|
*/
|
|
public function testLoginRedirectsToAdminOnSuccess(): void
|
|
{
|
|
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
|
|
|
$this->authService->method('checkRateLimit')->willReturn(0);
|
|
$this->authService->method('authenticate')->willReturn($user);
|
|
|
|
$this->authService->expects($this->once())->method('resetRateLimit');
|
|
$this->authService->expects($this->once())->method('login')->with($user);
|
|
|
|
$req = $this->makePost('/auth/login', ['username' => 'alice', 'password' => 'secret']);
|
|
$res = $this->controller->login($req, $this->makeResponse());
|
|
|
|
$this->assertRedirectTo($res, '/admin/posts');
|
|
}
|
|
|
|
/**
|
|
* login() doit utiliser '0.0.0.0' comme IP de repli si REMOTE_ADDR est absent.
|
|
*/
|
|
public function testLoginFallsBackToDefaultIpWhenRemoteAddrMissing(): void
|
|
{
|
|
$this->authService->method('checkRateLimit')->willReturn(0);
|
|
$this->authService->method('authenticate')->willReturn(null);
|
|
|
|
$this->authService->expects($this->once())
|
|
->method('checkRateLimit')
|
|
->with('0.0.0.0');
|
|
|
|
$req = $this->makePost('/auth/login');
|
|
$this->controller->login($req, $this->makeResponse());
|
|
}
|
|
|
|
// ── logout ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* logout() doit détruire la session et rediriger vers l'accueil.
|
|
*/
|
|
public function testLogoutDestroysSessionAndRedirects(): void
|
|
{
|
|
$this->authService->expects($this->once())->method('logout');
|
|
|
|
$res = $this->controller->logout($this->makePost('/auth/logout'), $this->makeResponse());
|
|
|
|
$this->assertRedirectTo($res, '/');
|
|
}
|
|
}
|