Files
compras/tests/Feature/Auth/TwoFactorFoundationTest.php
T
2026-11-03 11:51:44 +02:00

80 lines
4.1 KiB
PHP

<?php
use App\Models\User;
use Inertia\Testing\AssertableInertia as Assert;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Features;
use PragmaRX\Google2FA\Google2FA;
require_once __DIR__.'/../../Support/FortifyMfa.php';
test('two factor foundation remains disabled and secrets never enter user serialization', function () {
expect(Features::enabled(Features::twoFactorAuthentication()))->toBeFalse();
$user = User::factory()->create();
app(EnableTwoFactorAuthentication::class)($user);
expect($user->toArray())->not->toHaveKeys(['two_factor_secret', 'two_factor_recovery_codes']);
expect($user->two_factor_secret)->not->toBe(decrypt($user->two_factor_secret));
$this->get('/two-factor-challenge')->assertNotFound();
});
test('two factor foundation challenges a confirmed account and accepts a recovery code only once', function () {
foundationMfaRoutes();
$user = User::factory()->create();
app(EnableTwoFactorAuthentication::class)($user);
$user->forceFill(['two_factor_confirmed_at' => now()])->save();
$code = $user->recoveryCodes()[0];
$this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge');
$this->assertGuest();
$this->get('/two-factor-challenge')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private')
->assertInertia(fn (Assert $p) => $p->component('auth/two-factor-challenge')->missing('secret')->missing('recoveryCodes'));
$this->post('/two-factor-challenge', ['recovery_code' => $code])->assertRedirect();
$this->assertAuthenticatedAs($user);
expect($user->fresh()->recoveryCodes())->not->toContain($code);
$this->post('/logout');
$this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge');
$this->post('/two-factor-challenge', ['recovery_code' => $code])->assertSessionHasErrors();
$this->assertGuest();
});
test('two factor foundation rejects invalid codes and throttles repeated challenges', function () {
foundationMfaRoutes();
$user = User::factory()->create();
app(EnableTwoFactorAuthentication::class)($user);
$user->forceFill(['two_factor_confirmed_at' => now()])->save();
$this->withSession(['login.id' => $user->id]);
for ($i = 0; $i < 5; $i++) {
$this->postJson('/two-factor-challenge', ['recovery_code' => 'invalid'])->assertUnprocessable();
}
$this->postJson('/two-factor-challenge', ['recovery_code' => 'invalid'])->assertStatus(429);
$this->assertGuest();
});
test('two factor foundation accepts an authenticator code without exposing its secret', function () {
foundationMfaRoutes();
$user = User::factory()->create();
app(EnableTwoFactorAuthentication::class)($user);
$user->forceFill(['two_factor_confirmed_at' => now()])->save();
$this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge');
$code = (new Google2FA)->getCurrentOtp(decrypt($user->two_factor_secret));
$this->post('/two-factor-challenge', ['code' => $code])->assertRedirect();
$this->assertAuthenticatedAs($user);
});
test('two factor foundation does not challenge an unfinished enrollment', function () {
foundationMfaRoutes();
$user = User::factory()->create();
app(EnableTwoFactorAuthentication::class)($user);
expect($user->hasEnabledTwoFactorAuthentication())->toBeFalse();
$this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
});
test('two factor foundation password confirmation uses the native password check and private view', function () {
$user = User::factory()->create();
$this->actingAs($user)->get('/user/confirm-password')->assertOk()
->assertInertia(fn (Assert $p) => $p->component('auth/confirm-password'));
$this->post('/user/confirm-password', ['password' => 'wrong'])->assertSessionHasErrors('password');
$this->post('/user/confirm-password', ['password' => 'password'])->assertSessionHasNoErrors();
expect(session('auth.password_confirmed_at'))->not->toBeNull();
});