get('/2fa')->assertRedirect(route('login', absolute: false)); }); test('2fa view renders when 2fa session is set', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '654321'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->get('/2fa')->assertOk()->assertViewIs('auth.two-factor'); }); test('verify with correct code logs the user in', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '111222'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->post('/2fa', ['code' => '111222']) ->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticatedAs($user); }); test('verify with wrong code keeps user logged out', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '111222'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->post('/2fa', ['code' => '999999']) ->assertSessionHas('error'); $this->assertGuest(); }); test('verify with expired code redirects to login', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '111222'); Session::put('auth.2fa_expires_at', now()->subSecond()->timestamp); $this->post('/2fa', ['code' => '111222']) ->assertRedirect(route('login', absolute: false)) ->assertSessionHas('error'); $this->assertGuest(); }); test('verify rejects code with wrong length', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '111222'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->post('/2fa', ['code' => '12345'])->assertSessionHasErrors('code'); $this->assertGuest(); }); test('trust device option persists a trusted device row', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '888888'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->post('/2fa', ['code' => '888888', 'trust_device' => '1']); expect(UserTrustedDevice::where('user_id', $user->id)->count())->toBe(1); }); test('trust device defaults to no row when option is unset', function () { $user = User::factory()->create(); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '777777'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->post('/2fa', ['code' => '777777']); expect(UserTrustedDevice::where('user_id', $user->id)->count())->toBe(0); }); test('trusted device cookie skips 2fa view and auto-logs-in', function () { $user = User::factory()->create(); $deviceId = (string) Str::uuid(); $secret = Str::random(64); UserTrustedDevice::create([ 'user_id' => $user->id, 'device_id' => $deviceId, 'token' => hash('sha256', $secret), 'expires_at' => now()->addDays(30), ]); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '000000'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->withCookie('2fa_trust_device', $deviceId.'|'.$secret) ->get('/2fa') ->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticatedAs($user); }); test('trusted device cookie with wrong secret does not auto-login', function () { $user = User::factory()->create(); $deviceId = (string) Str::uuid(); $realSecret = Str::random(64); UserTrustedDevice::create([ 'user_id' => $user->id, 'device_id' => $deviceId, 'token' => hash('sha256', $realSecret), 'expires_at' => now()->addDays(30), ]); Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', '000000'); Session::put('auth.2fa_expires_at', now()->addMinutes(10)->timestamp); $this->withCookie('2fa_trust_device', $deviceId.'|wrong-secret') ->get('/2fa') ->assertOk(); $this->assertGuest(); });