approved()->create(); $response = $this->actingAs($owner)->postJson('/web-api/clients', [ 'name' => 'Password Grant Test Client', 'grant_type' => 'password', ]); $response->assertCreated(); $this->clientId = $response->json('client.id'); $this->clientSecret = $response->json('plain_secret'); } public function test_password_grant_still_works_for_users_without_two_factor(): void { User::factory()->approved()->create([ 'email' => 'no2fa@example.com', 'password' => Hash::make('password123'), ]); $this->requestToken('no2fa@example.com', 'password123') ->assertOk()->assertJsonStructure(['access_token']); } public function test_password_grant_is_rejected_without_otp_when_two_factor_is_enabled(): void { User::factory()->approved()->create([ 'email' => 'has2fa@example.com', 'password' => Hash::make('password123'), 'two_factor_secret' => (new Google2FA())->generateSecretKey(), 'two_factor_confirmed_at' => now(), ]); $this->requestToken('has2fa@example.com', 'password123')->assertStatus(400); } public function test_password_grant_is_rejected_with_wrong_otp(): void { User::factory()->approved()->create([ 'email' => 'has2fa@example.com', 'password' => Hash::make('password123'), 'two_factor_secret' => (new Google2FA())->generateSecretKey(), 'two_factor_confirmed_at' => now(), ]); $this->requestToken('has2fa@example.com', 'password123', '000000')->assertStatus(400); } public function test_password_grant_succeeds_with_valid_totp_code(): void { $secret = (new Google2FA())->generateSecretKey(); User::factory()->approved()->create([ 'email' => 'has2fa@example.com', 'password' => Hash::make('password123'), 'two_factor_secret' => $secret, 'two_factor_confirmed_at' => now(), ]); $otp = (new Google2FA())->getCurrentOtp($secret); $this->requestToken('has2fa@example.com', 'password123', $otp) ->assertOk()->assertJsonStructure(['access_token']); } public function test_password_grant_succeeds_with_a_recovery_code_and_consumes_it(): void { $user = User::factory()->approved()->create([ 'email' => 'has2fa@example.com', 'password' => Hash::make('password123'), 'two_factor_secret' => (new Google2FA())->generateSecretKey(), 'two_factor_recovery_codes' => ['AAAA-1111', 'BBBB-2222'], 'two_factor_confirmed_at' => now(), ]); $this->requestToken('has2fa@example.com', 'password123', 'AAAA-1111')->assertOk(); $this->assertNotContains('AAAA-1111', $user->fresh()->two_factor_recovery_codes); // El mismo código de recuperación no sirve una segunda vez. $this->requestToken('has2fa@example.com', 'password123', 'AAAA-1111')->assertStatus(400); } private function requestToken(string $email, string $password, ?string $otp = null) { return $this->postJson('/oauth/token', array_filter([ 'grant_type' => 'password', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'username' => $email, 'password' => $password, 'otp' => $otp, 'scope' => 'users:read', ], fn ($v) => $v !== null)); } }