CluPilotCloud/tests/Feature/Auth/OperatorTwoFactorChallengeT...

103 lines
3.4 KiB
PHP

<?php
use App\Livewire\Auth\OperatorTwoFactorChallenge;
use App\Models\Operator;
use Illuminate\Support\Facades\Auth;
use Livewire\Livewire;
use PragmaRX\Google2FA\Google2FA;
/**
* The operator's two-factor step, exercised directly.
*
* OperatorLoginTest proves the password step hands off to this challenge
* without signing anyone in; these tests prove the challenge itself only
* completes the sign-in once the code actually checks out — the other half of
* "not signed in before the code is verified".
*/
function pendingOperator(array $attributes = []): Operator
{
$operator = Operator::factory()->role('Owner')->create(array_merge([
'password' => 'richtig-langes-pw',
'two_factor_secret' => encrypt('JBSWY3DPEHPK3PXP'),
'two_factor_recovery_codes' => encrypt(json_encode(['aaaa-1111', 'bbbb-2222'])),
'two_factor_confirmed_at' => now(),
], $attributes));
session(['operator.2fa.id' => $operator->id, 'operator.2fa.remember' => false]);
return $operator;
}
it('is a dead end without a password step already taken', function () {
Livewire::test(OperatorTwoFactorChallenge::class)->assertForbidden();
expect(Auth::guard('operator')->check())->toBeFalse();
});
it('signs the operator in once the code actually checks out', function () {
$operator = pendingOperator();
$code = app(Google2FA::class)->getCurrentOtp(decrypt($operator->two_factor_secret));
Livewire::test(OperatorTwoFactorChallenge::class)
->set('code', $code)
->call('verify')
->assertRedirect(App\Support\AdminArea::home());
expect(Auth::guard('operator')->check())->toBeTrue()
->and(session()->has('operator.2fa.id'))->toBeFalse();
});
it('refuses a wrong code and leaves the operator signed out', function () {
$operator = pendingOperator();
Livewire::test(OperatorTwoFactorChallenge::class)
->set('code', '000000')
->call('verify')
->assertHasErrors('code');
expect(Auth::guard('operator')->check())->toBeFalse()
->and($operator->fresh()->last_login_at)->toBeNull();
});
it('accepts a recovery code exactly once', function () {
$operator = pendingOperator();
Livewire::test(OperatorTwoFactorChallenge::class)
->set('usingRecovery', true)
->set('recoveryCode', 'aaaa-1111')
->call('verify')
->assertRedirect(App\Support\AdminArea::home());
expect(Auth::guard('operator')->check())->toBeTrue();
$codes = json_decode(decrypt($operator->fresh()->two_factor_recovery_codes), true);
expect($codes)->not->toContain('aaaa-1111')
->and($codes)->toContain('bbbb-2222');
// Spent — signing back out and trying the same code again must fail.
Auth::guard('operator')->logout();
session(['operator.2fa.id' => $operator->id, 'operator.2fa.remember' => false]);
Livewire::test(OperatorTwoFactorChallenge::class)
->set('usingRecovery', true)
->set('recoveryCode', 'aaaa-1111')
->call('verify')
->assertHasErrors('code');
expect(Auth::guard('operator')->check())->toBeFalse();
});
it('throttles repeated wrong codes', function () {
pendingOperator();
$component = Livewire::test(OperatorTwoFactorChallenge::class)->set('code', '000000');
foreach (range(1, 5) as $ignored) {
$component->call('verify');
}
$component->call('verify')->assertHasErrors('code');
expect(Auth::guard('operator')->check())->toBeFalse();
});