Files
slim-blog/tests/Auth/AuthControllerTest.php
2026-03-16 13:40:18 +01:00

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, '/');
}
}