Files
portal/app/tests/Feature/Auth/ForgotPasswordTest.php
T
Дмитрий 170382878b phase2(forgot-password): POST /api/auth/forgot + ForgotPasswordView интеграция
- AuthController::forgotPassword использует Password::sendResetLink (anti-enumeration: всегда 200)
- AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets — указывает на нашу таблицу из schema v8.7
- Rate-limit 5/15мин по auth:forgot:{email}|{ip} — hit ставится ДО sendResetLink (защита перебора через unknown email)
- Frontend: authApi.forgotPassword, auth-store.requestPasswordReset, ForgotPasswordView success-state
- Pest +6 в ForgotPasswordTest (79/79 за 10.55с, 273 assertions)
- Vitest +4 (153/153 за 11.11с)
- TODO: POST /api/auth/reset-password + UI-форма new_password (deep-link)
- Регресс: lint+type+format OK; build 862ms; story:build 21/28 за 32с; Pint+Stan passed
- CLAUDE.md v1.36→v1.37, реестр v1.45→v1.46

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:10:28 +03:00

124 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
});
test('POST /api/auth/forgot для существующего email возвращает unified 200 + создаёт password_resets row + шлёт notification', function () {
Notification::fake();
$user = User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'real-user@example.ru',
'password_hash' => Hash::make('old-pass-1234'),
]);
$r = $this->postJson('/api/auth/forgot', [
'email' => 'real-user@example.ru',
]);
$r->assertOk();
expect($r->json('message'))->toContain('ссылку для сброса пароля');
// Token записан в password_resets (config: AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets).
$tokenRow = DB::table('password_resets')->where('email', 'real-user@example.ru')->first();
expect($tokenRow)->not->toBeNull();
expect($tokenRow->token)->not->toBeEmpty();
// Notification отправлен.
Notification::assertSentTo($user, ResetPassword::class);
});
test('POST /api/auth/forgot для НЕсуществующего email возвращает то же 200 (anti-enumeration)', function () {
Notification::fake();
$r = $this->postJson('/api/auth/forgot', [
'email' => 'unknown@example.ru',
]);
$r->assertOk();
expect($r->json('message'))->toContain('ссылку для сброса пароля');
// password_resets не должен содержать row для unknown email.
expect(DB::table('password_resets')->where('email', 'unknown@example.ru')->exists())->toBeFalse();
Notification::assertNothingSent();
});
test('POST /api/auth/forgot валидация email: 422 при невалидном формате', function () {
$r = $this->postJson('/api/auth/forgot', [
'email' => 'not-an-email',
]);
$r->assertStatus(422);
expect($r->json('errors.email'))->not->toBeEmpty();
});
test('POST /api/auth/forgot валидация email: 422 при пустом значении', function () {
$r = $this->postJson('/api/auth/forgot', []);
$r->assertStatus(422);
});
test('POST /api/auth/forgot rate-limit: 5 попыток / 15 мин', function () {
Notification::fake();
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'forgot-locked@example.ru',
'password_hash' => Hash::make('old-pass-1234'),
]);
// 5 успешных запросов (200 каждый — unified ответ).
for ($i = 1; $i <= 5; $i++) {
$this->postJson('/api/auth/forgot', [
'email' => 'forgot-locked@example.ru',
])->assertOk();
}
// 6-я → 429 + Retry-After.
$r = $this->postJson('/api/auth/forgot', [
'email' => 'forgot-locked@example.ru',
]);
$r->assertStatus(429);
expect($r->headers->get('Retry-After'))->not->toBeNull();
expect($r->json('retry_after'))->toBeGreaterThan(0);
});
test('POST /api/auth/forgot rate-limit изолирован по email — Bob не блокируется когда Alice заблокирована', function () {
Notification::fake();
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'alice-forgot@example.ru',
'password_hash' => Hash::make('alice-pass-1234'),
]);
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'bob-forgot@example.ru',
'password_hash' => Hash::make('bob-pass-1234'),
]);
// Alice — 5 раз → заблокирована.
for ($i = 1; $i <= 5; $i++) {
$this->postJson('/api/auth/forgot', ['email' => 'alice-forgot@example.ru'])->assertOk();
}
$this->postJson('/api/auth/forgot', ['email' => 'alice-forgot@example.ru'])->assertStatus(429);
// Bob проходит спокойно.
$this->postJson('/api/auth/forgot', ['email' => 'bob-forgot@example.ru'])->assertOk();
});