tenant = Tenant::factory()->create(); $this->google2fa = new Google2FA; $this->totpSecret = $this->google2fa->generateSecretKey(); $this->user = User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => '2fa-test@example.ru', 'password_hash' => Hash::make('secret-pass-123'), 'totp_enabled' => true, 'totp_secret' => $this->totpSecret, ]); }); test('login для 2FA-user НЕ создаёт authenticated session, возвращает requires_2fa=true', function () { $response = $this->postJson('/api/auth/login', [ 'email' => '2fa-test@example.ru', 'password' => 'secret-pass-123', ]); $response->assertOk(); $response->assertJsonPath('requires_2fa', true); $response->assertJsonPath('user.id', $this->user->id); // /me должен вернуть 401 — Auth::login НЕ был вызван при login для 2FA. $this->getJson('/api/auth/me')->assertStatus(401); }); test('verify2fa с правильным TOTP-кодом завершает login (200 + requires_2fa=false)', function () { // Step 1: login для 2FA-user. $this->postJson('/api/auth/login', [ 'email' => '2fa-test@example.ru', 'password' => 'secret-pass-123', ])->assertOk(); // Step 2: генерируем валидный TOTP-код для текущего timestep. $validCode = $this->google2fa->getCurrentOtp($this->totpSecret); $response = $this->postJson('/api/auth/2fa/verify', [ 'code' => $validCode, ]); $response->assertOk(); $response->assertJsonPath('requires_2fa', false); $response->assertJsonPath('user.id', $this->user->id); }); test('verify2fa с неверным кодом возвращает 422', function () { $this->postJson('/api/auth/login', [ 'email' => '2fa-test@example.ru', 'password' => 'secret-pass-123', ])->assertOk(); $response = $this->postJson('/api/auth/2fa/verify', [ 'code' => '000000', ]); $response->assertStatus(422); $response->assertJsonPath('errors.code.0', 'Неверный код.'); }); test('verify2fa без предварительного login возвращает 422 (нет pending_user_id)', function () { $validCode = $this->google2fa->getCurrentOtp($this->totpSecret); $response = $this->postJson('/api/auth/2fa/verify', [ 'code' => $validCode, ]); $response->assertStatus(422); $response->assertJsonPath('message', 'Сессия 2FA истекла. Войдите снова.'); }); test('verify2fa валидирует формат кода: 6 цифр', function () { $this->postJson('/api/auth/login', [ 'email' => '2fa-test@example.ru', 'password' => 'secret-pass-123', ])->assertOk(); // Слишком короткий. $this->postJson('/api/auth/2fa/verify', ['code' => '12345']) ->assertStatus(422) ->assertJsonValidationErrors(['code']); // Не цифры. $this->postJson('/api/auth/2fa/verify', ['code' => 'ABC123']) ->assertStatus(422) ->assertJsonValidationErrors(['code']); }); test('после успешной 2FA verify — последующий /me возвращает user (200)', function () { $this->postJson('/api/auth/login', [ 'email' => '2fa-test@example.ru', 'password' => 'secret-pass-123', ])->assertOk(); $validCode = $this->google2fa->getCurrentOtp($this->totpSecret); $this->postJson('/api/auth/2fa/verify', ['code' => $validCode])->assertOk(); $this->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('user.id', $this->user->id); });