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(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(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('refuses to complete the login when the operator was disabled after the password step', function () { // The realistic case: an owner revokes access WHILE someone is mid-login, // not before. The code entered here is genuinely correct — the account // it belongs to no longer qualifies. $operator = pendingOperator(); $code = app(Google2FA::class)->getCurrentOtp(decrypt($operator->two_factor_secret)); $operator->forceFill(['disabled_at' => now()])->save(); Livewire::test(OperatorTwoFactorChallenge::class) ->set('code', $code) ->call('verify') ->assertHasErrors('code'); expect(Auth::guard('operator')->check())->toBeFalse() ->and($operator->fresh()->last_login_at)->toBeNull(); }); it('refuses to complete the login when every console role was revoked after the password step', function () { $operator = pendingOperator(); $code = app(Google2FA::class)->getCurrentOtp(decrypt($operator->two_factor_secret)); $operator->syncRoles([]); Livewire::test(OperatorTwoFactorChallenge::class) ->set('code', $code) ->call('verify') ->assertHasErrors('code'); expect(Auth::guard('operator')->check())->toBeFalse() ->and($operator->fresh()->last_login_at)->toBeNull(); }); 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(); });