testSession; } protected function authService(): AuthService { return new AuthService($this->fakeDb, $this->config, $this->testSession, new PasswordHasher($this->config)); } } final class AuthControllerTest extends TestCase { /** @var list */ private array $touchedKeys = []; protected function setUp(): void { $this->setEnv('ACCOUNT_LOCKOUT_THRESHOLD', '5'); $this->setEnv('ACCOUNT_LOCKOUT_BASE_SECONDS', '60'); $this->setEnv('ACCOUNT_LOCKOUT_MAX_SECONDS', '900'); $this->setEnv('IP_THROTTLE_MAX_ATTEMPTS', '20'); $this->setEnv('IP_THROTTLE_WINDOW_SECONDS', '900'); $this->setEnv('ARGON2_MEMORY_COST', '1024'); $this->setEnv('ARGON2_TIME_COST', '1'); $this->setEnv('ARGON2_THREADS', '1'); } protected function tearDown(): void { foreach ($this->touchedKeys as $key) { putenv($key); } $this->touchedKeys = []; } private function setEnv(string $key, string $value): void { $this->touchedKeys[] = $key; putenv($key . '=' . $value); } /** * @param array $form */ private function postRequest(array $form, string $path = '/login'): Request { return new Request( 'POST', $path, [], ['content-type' => 'application/x-www-form-urlencoded'], http_build_query($form), '203.0.113.5', ); } private function getRequest(string $path = '/login'): Request { return new Request('GET', $path, [], [], '', '203.0.113.5'); } private function controller(Request $request, SessionManager $session, FakeDatabase $db): TestAuthController { return new TestAuthController($request, new Config(), new Database(new Config()), $session, $db); } /** * @param array $overrides * @return array */ private function userRow(string $password, array $overrides = []): array { return array_merge([ 'id' => 7, 'role_id' => 3, 'password_hash' => (new PasswordHasher(new Config()))->hash($password), 'failed_login_attempts' => 0, 'lockout_until' => null, 'default_route' => '/admin/dashboard', ], $overrides); } public function testShowLoginRendersCsrfField(): void { $session = new SessionManager(new Config(), true); $response = $this->controller($this->getRequest(), $session, new FakeDatabase())->showLogin(); self::assertSame(200, $response->status()); self::assertStringContainsString('name="_csrf"', $response->body()); } public function testLoginRejectsInvalidCsrfWith403(): void { $session = new SessionManager(new Config(), true); Csrf::token($session); $db = new FakeDatabase(); $request = $this->postRequest(['_csrf' => 'wrong', 'email' => 'admin@wakdo.local', 'password' => 'x']); $response = $this->controller($request, $session, $db)->login(); self::assertSame(403, $response->status()); // L'authentification n'a pas tourne : aucune ecriture base. self::assertSame([], $db->writes); } public function testLoginBadCredentialsRendersGenericErrorWithoutRedirect(): void { $session = new SessionManager(new Config(), true); $token = Csrf::token($session); $db = new FakeDatabase(); $db->userRow = $this->userRow('right-password'); $request = $this->postRequest(['_csrf' => $token, 'email' => 'admin@wakdo.local', 'password' => 'WRONG']); $response = $this->controller($request, $session, $db)->login(); self::assertSame(200, $response->status()); self::assertNull($response->header('Location')); self::assertStringContainsString('Email ou mot de passe incorrect', $response->body()); } public function testLoginSuccessRedirectsToDefaultRoute(): void { $session = new SessionManager(new Config(), true); $token = Csrf::token($session); $db = new FakeDatabase(); $db->userRow = $this->userRow('correct-password'); $request = $this->postRequest(['_csrf' => $token, 'email' => 'admin@wakdo.local', 'password' => 'correct-password']); $response = $this->controller($request, $session, $db)->login(); self::assertSame(302, $response->status()); self::assertSame('/admin/dashboard', $response->header('Location')); self::assertSame(7, $session->getInt('user_id')); } public function testLogoutRequiresValidCsrf(): void { $session = new SessionManager(new Config(), true); Csrf::token($session); $session->set('user_id', 7); $request = $this->postRequest(['_csrf' => 'wrong'], '/logout'); $response = $this->controller($request, $session, new FakeDatabase())->logout(); self::assertSame(403, $response->status()); // Session intacte : la deconnexion forgee est refusee. self::assertSame(7, $session->getInt('user_id')); } public function testLogoutWithValidCsrfClearsSessionAndRedirects(): void { $session = new SessionManager(new Config(), true); $token = Csrf::token($session); $session->set('user_id', 7); $request = $this->postRequest(['_csrf' => $token], '/logout'); $response = $this->controller($request, $session, new FakeDatabase())->logout(); self::assertSame(302, $response->status()); self::assertSame('/login', $response->header('Location')); self::assertNull($session->getInt('user_id')); } }