clusev/tests/Unit/Support/Confirm/ConfirmTokenTest.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);
}
}