79 lines
2.8 KiB
PHP
79 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Settings;
|
|
|
|
use App\Livewire\Settings\Security;
|
|
use App\Models\User;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use App\Support\Confirm\InvalidConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* The apply handler is the security gate: it only acts on a CONFIRMED, single-use
|
|
* token. A direct dispatch (bypassing the modal), an unconfirmed token, or a replay
|
|
* must all be a no-op.
|
|
*/
|
|
class SecurityConfirmTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function userWithTotp(): User
|
|
{
|
|
return User::factory()->create([
|
|
'two_factor_secret' => 'S',
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
}
|
|
|
|
public function test_direct_handler_call_with_a_garbage_token_does_not_disable_2fa(): void
|
|
{
|
|
$user = $this->userWithTotp();
|
|
|
|
Livewire::actingAs($user)->test(Security::class)->call('disableTwoFactor', 'forged-token');
|
|
|
|
$this->assertTrue($user->fresh()->hasTotp(), 'forged token must not disable 2FA');
|
|
}
|
|
|
|
public function test_an_unconfirmed_token_is_rejected_by_the_handler(): void
|
|
{
|
|
// Issuing a token (the opener) is not enough — the modal's confirm() must run
|
|
// first. This closes the "skip the confirmation modal" bypass.
|
|
$user = $this->userWithTotp();
|
|
$this->actingAs($user);
|
|
$token = ConfirmToken::issue('twoFactorDisabled', [], 'two_factor.disable', $user->email);
|
|
|
|
Livewire::actingAs($user->fresh())->test(Security::class)->call('disableTwoFactor', $token);
|
|
|
|
$this->assertTrue($user->fresh()->hasTotp(), 'a never-confirmed token must not apply');
|
|
}
|
|
|
|
public function test_a_confirmed_token_disables_2fa(): void
|
|
{
|
|
$user = $this->userWithTotp();
|
|
$this->actingAs($user);
|
|
$token = ConfirmToken::issue('twoFactorDisabled', [], 'two_factor.disable', $user->email);
|
|
ConfirmToken::confirm($token); // the modal confirmation step
|
|
|
|
Livewire::actingAs($user->fresh())->test(Security::class)->call('disableTwoFactor', $token);
|
|
|
|
$this->assertFalse($user->fresh()->hasTotp(), 'a confirmed token must disable 2FA');
|
|
}
|
|
|
|
public function test_the_handler_burns_the_token_so_it_cannot_be_replayed(): void
|
|
{
|
|
$user = $this->userWithTotp();
|
|
$this->actingAs($user);
|
|
$token = ConfirmToken::issue('twoFactorDisabled', [], 'two_factor.disable', $user->email);
|
|
ConfirmToken::confirm($token);
|
|
|
|
Livewire::actingAs($user->fresh())->test(Security::class)->call('disableTwoFactor', $token);
|
|
$this->assertFalse($user->fresh()->hasTotp());
|
|
|
|
// Burned: the same token can no longer be confirmed or consumed.
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::confirm($token);
|
|
}
|
|
}
|