137 lines
5.1 KiB
PHP
137 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api\Admin;
|
|
|
|
use App\Mail\Admin\AdminLoginAlertMail;
|
|
use App\Models\AdminLoginBlock;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Support\Auth\AdminLoginAlert;
|
|
use Database\Seeders\TemplateSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class LoginThrottleTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Cache::flush();
|
|
Mail::fake();
|
|
(new TemplateSeeder())->seedScenarioIfMissing('admin_login_alert');
|
|
}
|
|
|
|
private function login(string $username, string $password, string $ip = '127.0.0.1')
|
|
{
|
|
return $this->withServerVariables(['REMOTE_ADDR' => $ip])
|
|
->postJson('/api/v1/administrator/login', ['username' => $username, 'password' => $password]);
|
|
}
|
|
|
|
private function staff(): User
|
|
{
|
|
return User::factory()->create(['username' => 'staff', 'password' => Hash::make('correct-password-123')]);
|
|
}
|
|
|
|
public function test_ip_is_blocked_for_20_minutes_after_more_than_4_failures(): void
|
|
{
|
|
$this->staff();
|
|
|
|
for ($i = 4; $i >= 1; $i--) {
|
|
$this->login('staff', 'wrong')->assertUnauthorized()->assertJsonPath('remaining_attempts', $i);
|
|
}
|
|
|
|
$this->login('staff', 'wrong')->assertStatus(429);
|
|
$this->login('staff', 'correct-password-123')->assertStatus(429);
|
|
$this->login('someone-else', 'whatever')->assertStatus(429)->assertJsonPath('message', fn ($m) => str_starts_with($m, 'IP của bạn'));
|
|
|
|
$this->travel(21)->minutes();
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
}
|
|
|
|
public function test_successful_login_does_not_reset_failures_of_the_ip(): void
|
|
{
|
|
$this->staff();
|
|
User::factory()->create(['username' => 'victim', 'password' => Hash::make('victim-password-123')]);
|
|
|
|
for ($i = 0; $i < 4; $i++) {
|
|
$this->login('victim', 'guess-' . $i)->assertUnauthorized();
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
$this->app['auth']->forgetGuards();
|
|
}
|
|
|
|
$this->login('victim', 'guess-final')->assertStatus(429);
|
|
$this->assertTrue(AdminLoginBlock::where('kind', 'ip')->sole()->isBlocked());
|
|
}
|
|
|
|
public function test_account_is_locked_after_failures_from_different_ips(): void
|
|
{
|
|
$this->staff();
|
|
|
|
foreach (['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4'] as $ip) {
|
|
$this->login('staff', 'wrong', $ip)->assertUnauthorized();
|
|
}
|
|
$this->login('STAFF', 'wrong', '10.0.0.5')->assertStatus(429);
|
|
|
|
$this->login('staff', 'correct-password-123', '10.0.0.9')
|
|
->assertStatus(429)
|
|
->assertJsonPath('message', fn ($m) => str_starts_with($m, 'Tài khoản này'));
|
|
$this->login('another', 'whatever', '10.0.0.9')->assertUnauthorized();
|
|
}
|
|
|
|
public function test_successful_login_alerts_configured_recipients(): void
|
|
{
|
|
$this->staff();
|
|
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
Mail::assertQueued(AdminLoginAlertMail::class, fn ($mail) => $mail->hasTo('web@tgs.com.vn'));
|
|
|
|
Setting::updateOrCreate(['key' => 'email_settings'], ['value' => json_encode([AdminLoginAlert::SETTING_FIELD => ['it@tgs.com.vn']])]);
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
Mail::assertQueued(AdminLoginAlertMail::class, fn ($mail) => $mail->hasTo('it@tgs.com.vn'));
|
|
}
|
|
|
|
public function test_empty_recipient_list_disables_alert(): void
|
|
{
|
|
$this->staff();
|
|
Setting::updateOrCreate(['key' => 'email_settings'], ['value' => json_encode([AdminLoginAlert::SETTING_FIELD => []])]);
|
|
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
Mail::assertNothingQueued();
|
|
}
|
|
|
|
public function test_admin_can_list_and_unblock_ip(): void
|
|
{
|
|
$this->staff();
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->login('staff', 'wrong');
|
|
}
|
|
|
|
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
|
$admin = User::factory()->create();
|
|
$admin->assignRole('Full Administrator');
|
|
|
|
$items = collect($this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/login-blocks')
|
|
->assertOk()
|
|
->assertJsonCount(2, 'data.items')
|
|
->json('data.items'))->keyBy('kind');
|
|
$this->assertSame('127.0.0.1', $items['ip']['identifier']);
|
|
$this->assertSame('staff', $items['account']['identifier']);
|
|
|
|
$this->actingAs($admin, 'admin')->deleteJson("/api/v1/administrator/login-blocks/{$items['ip']['id']}")->assertOk();
|
|
$this->app['auth']->forgetGuards();
|
|
$this->login('staff', 'correct-password-123')->assertStatus(429);
|
|
|
|
$this->actingAs($admin, 'admin')->deleteJson("/api/v1/administrator/login-blocks/{$items['account']['id']}")->assertOk();
|
|
$this->app['auth']->forgetGuards();
|
|
$this->login('staff', 'correct-password-123')->assertOk();
|
|
}
|
|
}
|