Files
portal/app/tests/Feature/Auth/ForgotPasswordTest.php
T

124 lines
4.2 KiB
PHP
Raw Normal View History

<?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();
});