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