134 lines
4.1 KiB
PHP
134 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support\Confirm;
|
|
|
|
use App\Models\User;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use App\Support\Confirm\InvalidConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Tests\TestCase;
|
|
|
|
class ConfirmTokenTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function login(): User
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
return $user;
|
|
}
|
|
|
|
public function test_issue_confirm_consume_is_the_happy_path(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('keyRemoved', ['fingerprint' => 'ab:cd'], 'ssh_key.remove', 'box · key', 7);
|
|
|
|
// The modal confirms (pending → confirmed)…
|
|
$this->assertSame('keyRemoved', ConfirmToken::confirm($token)['event']);
|
|
|
|
// …then the handler consumes (confirmed → burned).
|
|
$payload = ConfirmToken::consume($token, 'keyRemoved');
|
|
$this->assertSame(['fingerprint' => 'ab:cd'], $payload['params']);
|
|
$this->assertSame('ssh_key.remove', $payload['auditAction']);
|
|
$this->assertSame('box · key', $payload['auditTarget']);
|
|
$this->assertSame(7, $payload['serverId']);
|
|
}
|
|
|
|
public function test_a_freshly_issued_token_cannot_be_consumed_without_confirmation(): void
|
|
{
|
|
// The modal-bypass guard: issuing a token (opener) does NOT authorize apply.
|
|
$this->login();
|
|
$token = ConfirmToken::issue('twoFactorDisabled');
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::consume($token, 'twoFactorDisabled');
|
|
}
|
|
|
|
public function test_issue_rejects_an_event_outside_the_allowlist(): void
|
|
{
|
|
$this->login();
|
|
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
ConfirmToken::issue('evilEvent', ['userId' => 1]);
|
|
}
|
|
|
|
public function test_a_token_can_be_confirmed_only_once(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('twoFactorDisabled');
|
|
|
|
ConfirmToken::confirm($token);
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::confirm($token);
|
|
}
|
|
|
|
public function test_a_confirmed_token_is_consumed_only_once(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('twoFactorDisabled');
|
|
ConfirmToken::confirm($token);
|
|
|
|
ConfirmToken::consume($token, 'twoFactorDisabled');
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::consume($token, 'twoFactorDisabled');
|
|
}
|
|
|
|
public function test_consume_rejects_a_token_minted_for_a_different_action(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('fileConfirmed', ['name' => 'x']);
|
|
ConfirmToken::confirm($token);
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::consume($token, 'keyRemoved');
|
|
}
|
|
|
|
public function test_confirm_rejects_a_garbage_token(): void
|
|
{
|
|
$this->login();
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::confirm('not-a-real-token');
|
|
}
|
|
|
|
public function test_consume_rejects_a_garbage_token(): void
|
|
{
|
|
$this->login();
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::consume('not-a-real-token', 'keyRemoved');
|
|
}
|
|
|
|
public function test_peek_returns_null_on_a_garbage_token_and_never_throws(): void
|
|
{
|
|
$this->login();
|
|
|
|
$this->assertNull(ConfirmToken::peek('not-a-real-token'));
|
|
}
|
|
|
|
public function test_peek_exposes_the_audit_target_for_display(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('keyRemoved', [], 'ssh_key.remove', 'box · key', 7);
|
|
|
|
$this->assertSame('box · key', ConfirmToken::peek($token)['auditTarget']);
|
|
}
|
|
|
|
public function test_a_token_is_bound_to_the_issuing_user(): void
|
|
{
|
|
$this->login();
|
|
$token = ConfirmToken::issue('twoFactorDisabled');
|
|
|
|
// A different operator must not be able to confirm Alice's token.
|
|
Auth::login(User::factory()->create());
|
|
|
|
$this->expectException(InvalidConfirmToken::class);
|
|
ConfirmToken::confirm($token);
|
|
}
|
|
}
|