true]); $kernel = app(Kernel::class); $kernel->appendMiddlewareToGroup('web', \PortalQaAccountSelection::class); $kernel->addToMiddlewarePriorityAfter(StartSession::class, \PortalQaAccountSelection::class); } private function restored(): User { $user = User::factory()->create(['email' => 'restored-source@source.invalid']); $user->forceFill(['two_factor_secret' => (new Encrypter(random_bytes(32), 'AES-256-CBC'))->encrypt('SYNTHETICONLY'), 'two_factor_confirmed_at' => now()])->save(); return $user; } public function test_restored_account_is_stopped_before_mfa_without_changing_its_row(): void { $user = $this->restored(); $original = hash('sha256', json_encode($user->fresh()->getRawOriginal(), JSON_THROW_ON_ERROR)); $this->post('/login', ['email' => $user->email, 'password' => 'password']) ->assertRedirect('/login')->assertSessionHasErrors('email')->assertSessionMissing('login.id'); $this->assertGuest(); $this->assertSame($original, hash('sha256', json_encode($user->fresh()->getRawOriginal(), JSON_THROW_ON_ERROR))); } public function test_json_restored_login_and_stale_challenge_remain_unauthenticated(): void { $user = $this->restored(); $this->postJson('/login', ['email' => $user->email, 'password' => 'password']) ->assertUnprocessable()->assertJsonValidationErrors('email')->assertSessionMissing('login.id'); $this->withSession(['login.id' => $user->id])->get('/two-factor-challenge') ->assertRedirect('/login')->assertSessionHasErrors('email')->assertSessionMissing('login.id'); $this->assertGuest(); } public function test_synthetic_login_requires_valid_mfa_then_recovers_after_logout(): void { $user = User::factory()->create(['email' => 'synthetic-investor@example.invalid']); app(EnableTwoFactorAuthentication::class)($user); $user->forceFill(['two_factor_confirmed_at' => now()])->save(); $secret = Fortify::currentEncrypter()->decrypt($user->two_factor_secret); $credentials = ['email' => $user->email, 'password' => 'password']; $this->post('/login', $credentials)->assertRedirect('/two-factor-challenge'); $this->assertGuest(); $this->post('/two-factor-challenge', ['code' => 'invalid'])->assertRedirect('/two-factor-challenge'); $this->assertGuest(); $engine = new Google2FA; $this->post('/two-factor-challenge', ['code' => $engine->getCurrentOtp($secret)])->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); $this->get('/dashboard')->assertOk(); $this->post('/logout')->assertRedirect('/'); $this->assertGuest(); $this->post('/login', $credentials)->assertRedirect('/two-factor-challenge'); // A new login uses a fresh code in Fortify's normal accepted window, not the consumed code. $this->post('/two-factor-challenge', ['code' => $engine->oathTotp($secret, $engine->getTimestamp() + 1)])->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); } public function test_qa_account_policy_does_not_apply_when_flag_is_absent(): void { config(['portal.qa_synthetic_accounts_only' => false]); $user = User::factory()->create(['email' => 'ordinary-user@source.invalid']); $this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); } }