Merge remote-tracking branch 'OpensourcePOS/master' into feature-stock-location-dropdown-order

This commit is contained in:
Travis Garrison committed 2026-08-19 10:39:21 +04:00
commit a8219942e2
51 files changed
+395 -3

No files matched your search

+1 -1
View File
@@ -86,5 +86,5 @@ auth.json
/writable/logs/*.log
/writable/debugbar/*.json
/app/Database/database.sql
/writable/cache/settings
/writable/cache/*
/.env.bak
+5 -1
View File
@@ -2,6 +2,7 @@
namespace Config;
use App\Filters\Throttle;
use CodeIgniter\Config\Filters as BaseFilters;
use CodeIgniter\Filters\Cors;
use CodeIgniter\Filters\CSRF;
@@ -34,6 +35,7 @@ class Filters extends BaseFilters
'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class,
'throttle' => Throttle::class,
];
/**
@@ -107,7 +109,9 @@ class Filters extends BaseFilters
*
* @var array<string, array<string, list<string>>>
*/
public array $filters = [];
public array $filters = [
'throttle' => ['before' => ['login', 'migrate']],
];
/**
* Constructor to conditionally disable CSRF filter in testing environment
+1 -1
View File
@@ -76,7 +76,7 @@ class Login extends BaseController
];
if (!$this->validate($rules, $messages)) {
$data['has_errors'] = !empty($validation->getErrors());
$data['hasErrors'] = !empty($validation->getErrors());
return view('login', $data);
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
/**
* Rate limits login/migrate POST attempts, keyed by IP and by submitted
* username, to mitigate brute-force and credential-stuffing attacks
* (GHSA-hm9c-xchj-xgcp). Backed by CodeIgniter's cache-based Throttler,
* so limits are per-server (not shared across nodes on file cache).
*/
class Throttle implements FilterInterface
{
private const CAPACITY = 5;
private const SECONDS = 60;
public function before(RequestInterface $request, $arguments = null)
{
if ($request->getMethod() !== 'POST') {
return null;
}
$throttler = Services::throttler();
$ipKey = 'login-ip-' . $request->getIPAddress();
$username = strtolower((string) $request->getPost('username'));
$usernameKey = $username !== '' ? 'login-user-' . $username : null;
$ipOk = $throttler->check($ipKey, self::CAPACITY, self::SECONDS);
$usernameOk = $usernameKey === null || $throttler->check($usernameKey, self::CAPACITY, self::SECONDS);
if (!$ipOk || !$usernameOk) {
log_message('warning', 'Login throttled for IP {ip} (username: {username})', [
'ip' => $request->getIPAddress(),
'username' => $username !== '' ? $username : '(none)',
]);
return service('response')
->setStatusCode(429)
->setJSON([
'success' => false,
'message' => lang('Login.too_many_attempts'),
]);
}
return null;
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
return null;
}
}
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "كلمة السر",
"required_username" => "",
'too_many_attempts' => 'تم تجاوز عدد المحاولات المسموح بها. يرجى الانتظار قليلاً ثم المحاولة مرة أخرى.',
"username" => "اسم المستخدم",
"welcome" => "مرحباً بك في{0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "كلمة السر",
"required_username" => "خانة أسم المستخدم مطلوبة.",
'too_many_attempts' => 'عدد كبير جدًا من المحاولات. يرجى الانتظار لحظة والمحاولة مرة أخرى.',
"username" => "اسم المستخدم",
"welcome" => "مرحباً بك في{0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Şifrə",
"required_username" => "",
'too_many_attempts' => 'Həddindən artıq cəhd. Zəhmət olmasa bir az gözləyib yenidən cəhd edin.',
"username" => "İstifadəçi",
"welcome" => "{0} -ə xoş gəlmisiniz!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Password",
"required_username" => "",
'too_many_attempts' => 'Твърде много опити. Моля, изчакайте малко и опитайте отново.',
"username" => "Username",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Lozinka",
"required_username" => "",
'too_many_attempts' => 'Previše pokušaja. Molimo pričekajte trenutak i pokušajte ponovo.',
"username" => "Korisničko ime",
"welcome" => "Dobrodošli u {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "وشەی نهێنی",
"required_username" => "خانەی ناوی بەکارهێنەر پێویستە.",
'too_many_attempts' => 'هەوڵدانی زۆر زیادە. تکایە کەمێک چاوەڕێ بکە و دووبارە هەوڵ بدەرەوە.',
"username" => "ناوی بەکارهێنەر",
"welcome" => "بەخێربێن بۆ {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Heslo",
"required_username" => "",
'too_many_attempts' => 'Příliš mnoho pokusů. Počkejte chvíli a zkuste to znovu.',
"username" => "Uživatelské jméno",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'For mange forsøg. Vent et øjeblik, og prøv igen.',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Datenbank-Migrationen werden ausgeführt...",
"password" => "Passwort",
"required_username" => "Das Feld Benutzername ist erforderlich.",
'too_many_attempts' => 'Zu viele Versuche. Bitte warten Sie einen Moment und versuchen Sie es erneut.',
"username" => "Benutzername",
"welcome" => "Willkommen bei {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Datenbank-Migrationen werden ausgeführt...",
"password" => "Passwort",
"required_username" => "Das Feld Benutzername ist erforderlich.",
'too_many_attempts' => 'Zu viele Versuche. Bitte warten Sie einen Moment und versuchen Sie es erneut.',
"username" => "Benutzername",
"welcome" => "Willkommen bei {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'Πάρα πολλές προσπάθειες. Παρακαλώ περιμένετε λίγο και δοκιμάστε ξανά.',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Running database migrations...",
"password" => "Password",
"required_username" => "The username field is required.",
'too_many_attempts' => 'Too many attempts. Please wait a moment and try again.',
"username" => "Username",
"welcome" => "Welcome to {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Running database migrations...",
"password" => "Password",
"required_username" => "The username field is required.",
'too_many_attempts' => 'Too many attempts. Please wait a moment and try again.',
"username" => "Username",
"welcome" => "Welcome to {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Ejecutando migraciones de base de datos...",
"password" => "Contraseña",
"required_username" => "El campo de nombre de usuario es obligatorio.",
'too_many_attempts' => 'Demasiados intentos. Por favor, espere un momento e inténtelo de nuevo.',
"username" => "Usuario",
"welcome" => "Bienvenido a {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Ejecutando migraciones de base de datos...",
"password" => "Contraseña",
"required_username" => "El nombre de usuario es obligatorio.",
'too_many_attempts' => 'Demasiados intentos. Por favor espera un momento e intenta de nuevo.',
"username" => "Usuario",
"welcome" => "Bienvenido a {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "کلمه عبور",
"required_username" => "",
'too_many_attempts' => 'تعداد تلاش‌ها بیش از حد مجاز است. لطفاً کمی صبر کنید و دوباره امتحان کنید.',
"username" => "نام کاربری",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Exécution des migrations de la base de données...",
"password" => "Mot de passe",
"required_username" => "Le champ nom utilisateur est obligatoire.",
'too_many_attempts' => 'Trop de tentatives. Veuillez patienter un instant et réessayer.',
"username" => "Nom d'utilisateur",
"welcome" => "Bienvenue à {0} !"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "סיסמה",
"required_username" => "",
'too_many_attempts' => 'יותר מדי ניסיונות. אנא המתן רגע ונסה שוב.',
"username" => "שם משתמש",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Lozinka",
"required_username" => "",
'too_many_attempts' => 'Previše pokušaja. Pričekajte trenutak i pokušajte ponovno.',
"username" => "Korisničko ime",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Jelszó",
"required_username" => "",
'too_many_attempts' => 'Túl sok próbálkozás. Kérjük, várjon egy kicsit, majd próbálja újra.',
"username" => "Felhasználó név",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'Չափազանց շատ փորձեր: Խնդրում ենք սպասել մի փոքր և կրկին փորձել:',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Kata kunci",
"required_username" => "Kolom nama pengguna wajib diisi.",
'too_many_attempts' => 'Terlalu banyak percobaan. Harap tunggu sebentar dan coba lagi.',
"username" => "Nama Anda",
"welcome" => "Selamat Datang di {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Esecuzione migrazioni database...",
"password" => "Password",
"required_username" => "Il campo nome utente è obbligatorio.",
'too_many_attempts' => 'Troppi tentativi. Attendere un momento e riprovare.',
"username" => "Username",
"welcome" => "Benvenuto in {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "ពាក្យសំងាត់",
"required_username" => "",
'too_many_attempts' => 'ការព្យាយាមច្រើនពេក។ សូមរង់ចាំបន្តិច ហើយព្យាយាមម្តងទៀត។',
"username" => "ឈ្មោះ",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Password",
"required_username" => "",
'too_many_attempts' => 'ພະຍາຍາມຫຼາຍເກີນໄປ. ກະລຸນາລໍຖ້າບຶດໜຶ່ງ ແລ້ວລອງໃໝ່ອີກຄັ້ງ.',
"username" => "Username",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'വളരെയധികം ശ്രമങ്ങൾ. ദയവായി അല്പസമയം കാത്തിരുന്ന് വീണ്ടും ശ്രമിക്കുക.',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'For mange forsøk. Vennligst vent litt og prøv igjen.',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Databasemigraties uitvoeren...",
"password" => "Paswoord",
"required_username" => "",
'too_many_attempts' => 'Te veel pogingen. Wacht even en probeer het opnieuw.',
"username" => "Gebruiker",
"welcome" => "Welkom op {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Databasemigraties uitvoeren...",
"password" => "Wachtwoord",
"required_username" => "Het gebruikersnaam veld is verplicht.",
'too_many_attempts' => 'Te veel pogingen. Wacht even en probeer het opnieuw.',
"username" => "Gebruikersnaam",
"welcome" => "Welkom bij {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Hasło",
"required_username" => "",
'too_many_attempts' => 'Zbyt wiele prób. Proszę chwilę poczekać i spróbować ponownie.',
"username" => "Nazwa użytkownika",
"welcome" => "Witaj w {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Executando migrações do banco de dados...",
"password" => "Senha",
"required_username" => "O campo nome de usuário é obrigatório.",
'too_many_attempts' => 'Muitas tentativas. Aguarde um momento e tente novamente.',
"username" => "Usuário",
"welcome" => "Bem-vindo ao {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'Prea multe încercări. Vă rugăm așteptați un moment și încercați din nou.',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Пароль",
"required_username" => "",
'too_many_attempts' => 'Слишком много попыток. Пожалуйста, подождите немного и попробуйте снова.',
"username" => "Имя пользователя",
"welcome" => "Панель управления"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Kör databasmigrationer...",
"password" => "Lösenord",
"required_username" => "Fältet för användarnamn krävs.",
'too_many_attempts' => 'För många försök. Vänta en stund och försök igen.',
"username" => "Användarnamn",
"welcome" => "Välkommen till {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Nenosiri",
"required_username" => "Jina la mtumiaji ni lazima.",
'too_many_attempts' => 'Majaribio mengi mno. Tafadhali subiri kidogo kisha ujaribu tena.',
"username" => "Jina la Mtumiaji",
"welcome" => "Karibu kwenye {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Nenosiri",
"required_username" => "Jina la mtumiaji ni lazima.",
'too_many_attempts' => 'Majaribio mengi mno. Tafadhali subiri kidogo kisha ujaribu tena.',
"username" => "Jina la Mtumiaji",
"welcome" => "Karibu kwenye {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "கடவுச்சொல்",
"required_username" => "",
'too_many_attempts' => 'பல முறை முயற்சி செய்யப்பட்டது. தயவுசெய்து சிறிது நேரம் காத்திருந்து மீண்டும் முயற்சிக்கவும்.',
"username" => "பயனர்பெயர்",
"welcome" => "{0} க்கு வருக!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "รหัสผ่าน",
"required_username" => "จำเป็นต้องระบุชื่อผู้ใช้งาน",
'too_many_attempts' => 'พยายามมากเกินไป กรุณารอสักครู่แล้วลองใหม่อีกครั้ง',
"username" => "ชื่อผู้ใช้",
"welcome" => "ยินดีต้อนรับสู่ {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Password",
"required_username" => "",
'too_many_attempts' => 'Masyadong maraming pagtatangka. Mangyaring maghintay sandali at subukang muli.',
"username" => "Username",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "Veritabanı göçleri çalışıyor...",
"password" => "Parola",
"required_username" => "Kullanıcı adı alanı gereklidir.",
'too_many_attempts' => 'Çok fazla deneme yapıldı. Lütfen bir süre bekleyip tekrar deneyin.',
"username" => "Kullanıcı Adı",
"welcome" => "{0}'e Hoş Geldiniz!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Пароль",
"required_username" => "",
'too_many_attempts' => 'Забагато спроб. Будь ласка, зачекайте трохи і спробуйте знову.',
"username" => "Ім'я користувача",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "",
"required_username" => "",
'too_many_attempts' => 'بہت زیادہ کوششیں۔ براہ کرم تھوڑی دیر انتظار کریں اور دوبارہ کوشش کریں۔',
"username" => "",
"welcome" => ""
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "Mật khẩu",
"required_username" => "",
'too_many_attempts' => 'Quá nhiều lần thử. Vui lòng đợi một chút và thử lại.',
"username" => "Tài khoản",
"welcome" => "Chào mừng đến với {0}!"
];
+1
View File
@@ -24,6 +24,7 @@ return [
"migration_required" => "",
"migration_running" => "",
"password" => "密码",
'too_many_attempts' => '太多次尝试。请稍等片刻后重试。',
"username" => "用户名",
"welcome" => "欢迎来到 {0}!"
];
+1
View File
@@ -25,6 +25,7 @@ return [
"migration_running" => "",
"password" => "密碼",
"required_username" => "",
'too_many_attempts' => '嘗試次數過多。請稍候片刻後再試一次。',
"username" => "帳號",
"welcome" => "歡迎來到 {0}"
];
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace Tests\Controllers;
use CodeIgniter\Config\Services;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
/**
* Test suite for the Login controller, including the CI Throttler
* mitigation for brute-force/credential-stuffing (GHSA-hm9c-xchj-xgcp).
*/
class LoginTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use FeatureTestTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $refresh = false;
protected $namespace = null;
private array $usedKeys = [];
protected function setUp(): void
{
parent::setUp();
Services::session()->destroy();
$this->clearThrottleState();
}
protected function tearDown(): void
{
$this->clearThrottleState();
parent::tearDown();
}
private function clearThrottleState(): void
{
$throttler = Services::throttler();
foreach ($this->usedKeys as $key) {
$throttler->remove($key);
}
$this->usedKeys = [];
Services::resetSingle('throttler');
}
private function trackThrottleKeys(string $username = ''): void
{
$this->usedKeys[] = 'login-ip-' . Services::request()->getIPAddress();
if ($username !== '') {
$this->usedKeys[] = 'login-user-' . strtolower($username);
}
}
/**
* Login::index() redirects to 'login' and runs a migration when the
* app's migration state isn't current, bypassing credential checks
* entirely. Skip credential-dependent assertions in that case so this
* test only exercises login logic when the DB is actually current
* (throttling itself is verified independently and runs before this
* branch, so it's unaffected).
*/
private function skipIfMigrationRequired(\CodeIgniter\Test\TestResponse $response): void
{
$redirectUrl = $response->getRedirectUrl();
if ($redirectUrl !== null && str_ends_with(rtrim(strtolower($redirectUrl), '/'), '/login')) {
$this->markTestSkipped('App migration state is not current in this environment; skipping credential-path assertions.');
}
}
public function testValidCredentialsLogsIn(): void
{
$response = $this->post('/login', [
'username' => 'admin',
'password' => 'pointofsale',
]);
$this->trackThrottleKeys('admin');
$this->skipIfMigrationRequired($response);
$response->assertRedirectTo('home');
}
public function testInvalidCredentialsShowsError(): void
{
$response = $this->post('/login', [
'username' => 'admin',
'password' => 'wrongpassword',
]);
$this->trackThrottleKeys('admin');
$this->skipIfMigrationRequired($response);
$response->assertStatus(200);
$response->assertSee(lang('Login.invalid_username_and_password'));
}
public function testSixthFailedAttemptIsThrottled(): void
{
for ($i = 0; $i < 5; $i++) {
$this->post('/login', [
'username' => 'wronguser',
'password' => 'wrongpassword',
]);
$this->trackThrottleKeys('wronguser');
}
$response = $this->post('/login', [
'username' => 'wronguser',
'password' => 'wrongpassword',
]);
$this->trackThrottleKeys('wronguser');
$response->assertStatus(429);
$result = json_decode($response->getJSON(), true);
$this->assertFalse($result['success']);
$this->assertSame(lang('Login.too_many_attempts'), $result['message']);
}
public function testCorrectLoginStillWorksUnderThrottleCapacity(): void
{
for ($i = 0; $i < 3; $i++) {
$this->post('/login', [
'username' => 'admin',
'password' => 'wrongpassword',
]);
$this->trackThrottleKeys('admin');
}
$response = $this->post('/login', [
'username' => 'admin',
'password' => 'pointofsale',
]);
$this->trackThrottleKeys('admin');
$this->skipIfMigrationRequired($response);
$response->assertRedirectTo('home');
}
public function testGetRequestToLoginIsNeverThrottled(): void
{
for ($i = 0; $i < 10; $i++) {
$response = $this->get('/login');
$this->skipIfMigrationRequired($response);
$response->assertStatus(200);
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace Tests\Filters;
use App\Filters\Throttle;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class ThrottleTest extends CIUnitTestCase
{
private Throttle $filter;
private array $usedKeys = [];
protected function setUp(): void
{
parent::setUp();
$this->filter = new Throttle();
}
protected function tearDown(): void
{
$throttler = Services::throttler();
foreach ($this->usedKeys as $key) {
$throttler->remove($key);
}
parent::tearDown();
}
private function makeRequest(string $method, string $ip, ?string $username = null): IncomingRequest
{
$request = $this->createMock(IncomingRequest::class);
$request->method('getMethod')->willReturn($method);
$request->method('getIPAddress')->willReturn($ip);
$request->method('getPost')->with('username')->willReturn($username);
$this->usedKeys[] = 'login-ip-' . $ip;
if ($username !== null && $username !== '') {
$this->usedKeys[] = 'login-user-' . strtolower($username);
}
return $request;
}
public function testGetRequestsAreNotThrottled(): void
{
$request = $this->makeRequest('GET', '203.0.113.1', 'admin');
$result = $this->filter->before($request);
$this->assertNull($result);
}
public function testAllowsAttemptsUnderCapacity(): void
{
$ip = '203.0.113.2';
for ($i = 0; $i < 5; $i++) {
$request = $this->makeRequest('POST', $ip, 'employee1');
$result = $this->filter->before($request);
$this->assertNull($result, "Attempt {$i} should not be throttled");
}
}
public function testBlocksAfterCapacityExceededByIp(): void
{
$ip = '203.0.113.3';
for ($i = 0; $i < 5; $i++) {
$this->filter->before($this->makeRequest('POST', $ip, "user{$i}"));
}
$result = $this->filter->before($this->makeRequest('POST', $ip, 'user-final'));
$this->assertNotNull($result);
$this->assertSame(429, $result->getStatusCode());
}
public function testBlocksAfterCapacityExceededByUsername(): void
{
$username = 'sameuser';
for ($i = 0; $i < 5; $i++) {
$this->filter->before($this->makeRequest('POST', "203.0.113.{$i}", $username));
}
$result = $this->filter->before($this->makeRequest('POST', '203.0.113.99', $username));
$this->assertNotNull($result);
$this->assertSame(429, $result->getStatusCode());
}
public function testThrottledResponseIsJsonWithMessage(): void
{
$ip = '203.0.113.4';
for ($i = 0; $i < 5; $i++) {
$this->filter->before($this->makeRequest('POST', $ip, 'someone'));
}
$result = $this->filter->before($this->makeRequest('POST', $ip, 'someone'));
$body = json_decode($result->getJSON(), true);
$this->assertFalse($body['success']);
$this->assertSame(lang('Login.too_many_attempts'), $body['message']);
}
public function testMissingUsernameOnlyThrottlesByIp(): void
{
$ip = '203.0.113.5';
for ($i = 0; $i < 5; $i++) {
$this->filter->before($this->makeRequest('POST', $ip, null));
}
$result = $this->filter->before($this->makeRequest('POST', $ip, null));
$this->assertNotNull($result);
$this->assertSame(429, $result->getStatusCode());
}
}