1- <?php namespace Tests ;
1+ <?php
2+ namespace Tests ;
23/**
34 * Copyright 2026 OpenStack Foundation
45 * Licensed under the Apache License, Version 2.0 (the "License");
2425 * - cf-turnstile-response required when login_attempts (from request body) >= threshold
2526 * - threshold gating (before / at boundary / above boundary)
2627 * - omitted login_attempts field defaults to zero (no captcha required)
27- * - captcha is gated on the request-body counter, not on DB state
28+ * - captcha is gated on the request-body counter
2829 * - login screen emits Turnstile JS config after a failed attempt
2930 * - expired or unsolved token is rejected
3031 */
3132final class UserLoginTurnstileTest extends BrowserKitTestCase
3233{
3334 private const LOGIN_URL = '/auth/login ' ;
3435 // Matches ServerConfigurationService::DefaultMaxFailedLoginAttempts2ShowCaptcha
35- private const CAPTCHA_THRESHOLD = 3 ;
36+ private const CAPTCHA_THRESHOLD = 3 ;
3637
3738 private string $ testEmail ;
3839 private string $ testPassword ;
3940
4041 protected function prepareForTests (): void
4142 {
4243 parent ::prepareForTests ();
43- $ this ->testEmail = env ('TEST_USER_EMAIL ' );
44+ $ this ->testEmail = env ('TEST_USER_EMAIL ' );
4445 $ this ->testPassword = env ('TEST_USER_PASSWORD ' );
4546 if (empty ($ this ->testEmail ) || empty ($ this ->testPassword )) {
4647 $ this ->markTestSkipped ('TEST_USER_EMAIL and TEST_USER_PASSWORD env vars are required. ' );
@@ -67,8 +68,8 @@ private function postLogin(array $overrides = [])
6768 return $ this ->call ('POST ' , self ::LOGIN_URL , array_merge ([
6869 'username ' => $ this ->testEmail ,
6970 'password ' => $ this ->testPassword ,
70- 'flow ' => 'password ' ,
71- '_token ' => Session::token (),
71+ 'flow ' => 'password ' ,
72+ '_token ' => Session::token (),
7273 ], $ overrides ));
7374 }
7475
@@ -138,7 +139,7 @@ public function testLoginAtThresholdWithValidTokenPassesValidation(): void
138139
139140 $ this ->postLogin ([
140141 'cf-turnstile-response ' => 'dummy-token-accepted-by-mock ' ,
141- 'login_attempts ' => 1
142+ 'login_attempts ' => self :: CAPTCHA_THRESHOLD
142143 ]);
143144
144145 $ this ->assertFalse (
@@ -162,23 +163,6 @@ public function testOmittedLoginAttemptsFieldDefaultsToZeroNoCaptchaRequired():
162163 );
163164 }
164165
165- public function testCaptchaGatingUsesRequestBodyCounterNotDbState (): void
166- {
167- // Even for a username that doesn't exist in the DB, if the request body
168- // carries login_attempts >= threshold the captcha rule must fire.
169- // This proves gating is driven by the request-body counter, not by DB lookup.
170- $ this ->postLogin ([
171- 'username ' => 'nobody@doesnotexist.example ' ,
172- 'password ' => 'irrelevant ' ,
173- 'login_attempts ' => self ::CAPTCHA_THRESHOLD ,
174- ]); // no cf-turnstile-response
175-
176- $ this ->assertTrue (
177- $ this ->sessionHasValidationError ('cf-turnstile-response ' ),
178- 'Turnstile must be required when login_attempts >= threshold, regardless of whether the user exists '
179- );
180- }
181-
182166 // -------------------------------------------------------------------------
183167 // 4. Rendering of Turnstile on the thresholded login screen
184168 // -------------------------------------------------------------------------
@@ -241,4 +225,4 @@ public function testUnsolvedCaptchaEmptyTokenFailsValidation(): void
241225 'An empty Turnstile response must be rejected by the required rule '
242226 );
243227 }
244- }
228+ }
0 commit comments