Files
portal/app/tests/Feature/Auth/RegisterFlowTest.php
T
Дмитрий 564d984f2a fix(auth): registerResend — общий часовой лимит отправок (review)
Backend-ревью: register/resend применял только cooldown 60с, но не часовой
лимит 5/час (spec §7.6) — можно было слать код 1/мин бесконечно. Добавлен
RateLimiter по ключу email|ip (как в registerStart). +тест throttle для start.
Larastan baseline перегенерирован (новый тест добавил postJson-вызовы).

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

201 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Mail\RegisterEmailVerificationCode;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
Mail::fake();
});
test('RegisterEmailVerificationCode содержит код и тему', function () {
$mailable = new RegisterEmailVerificationCode('123456');
$mailable->assertHasSubject('Код подтверждения регистрации — Лидерра');
$mailable->assertSeeInHtml('123456');
});
test('register/start принимает валидную форму, шлёт код, аккаунт ещё не создан', function () {
$response = $this->postJson('/api/auth/register/start', [
'email' => 'newcomer@example.ru',
'phone' => '+7 (912) 345-67-89',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
]);
$response->assertOk();
$response->assertJsonPath('email', 'newcomer@example.ru');
Mail::assertSent(RegisterEmailVerificationCode::class);
expect(User::where('email', 'newcomer@example.ru')->exists())->toBeFalse();
});
test('register/start отвергает существующий email', function () {
User::factory()->create(['tenant_id' => $this->tenant->id, 'email' => 'dup@example.ru']);
$this->postJson('/api/auth/register/start', [
'email' => 'dup@example.ru',
'phone' => '+7 (912) 345-67-89',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
])->assertStatus(422)->assertJsonValidationErrors(['email']);
});
test('register/start требует корректный телефон', function () {
$this->postJson('/api/auth/register/start', [
'email' => 'badphone@example.ru',
'phone' => '12345',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
])->assertStatus(422)->assertJsonValidationErrors(['phone']);
$this->postJson('/api/auth/register/start', [
'email' => 'nophone@example.ru',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
])->assertStatus(422)->assertJsonValidationErrors(['phone']);
});
test('register/start требует пароль ≥8 и оба согласия', function () {
$this->postJson('/api/auth/register/start', [
'email' => 'weak@example.ru', 'phone' => '+7 (912) 345-67-89',
'password' => 'short', 'accept_offer' => true, 'accept_pdn' => true,
])->assertStatus(422)->assertJsonValidationErrors(['password']);
$this->postJson('/api/auth/register/start', [
'email' => 'noconsent@example.ru', 'phone' => '+7 (912) 345-67-89',
'password' => 'fresh-pass-123', 'accept_pdn' => true,
])->assertStatus(422)->assertJsonValidationErrors(['accept_offer']);
});
test('register/start ограничивает число отправок кода (5/час по email|ip)', function () {
$payload = [
'email' => 'throttle@example.ru',
'phone' => '+7 (912) 345-67-89',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
];
// 5 отправок разрешены (аккаунт не создаётся до verify, email остаётся свободным).
for ($i = 0; $i < 5; $i++) {
$this->postJson('/api/auth/register/start', $payload)->assertOk();
}
// 6-я — превышение лимита.
$this->postJson('/api/auth/register/start', $payload)->assertStatus(429);
});
// ---------------------------------------------------------------------------
// Task 4: register/verify
// ---------------------------------------------------------------------------
/**
* Делает register/start и возвращает 6-значный код из отправленного письма.
*
* @param array<string, mixed> $overrides
*/
$startAndGetCode = function (array $overrides = []): string {
/** @var TestCase $this */
$payload = array_merge([
'email' => 'verify-flow@example.ru',
'phone' => '+7 (912) 345-67-89',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
], $overrides);
test()->postJson('/api/auth/register/start', $payload)->assertOk();
return Mail::sent(RegisterEmailVerificationCode::class)->first()->code;
};
test('register/verify создаёт аккаунт с подтверждённой почтой и нормализованным телефоном', function () use ($startAndGetCode) {
$code = $startAndGetCode();
$response = $this->postJson('/api/auth/register/verify', ['code' => $code]);
$response->assertStatus(201);
$response->assertJsonPath('user.email', 'verify-flow@example.ru');
$response->assertJsonPath('requires_2fa', false);
$user = User::where('email', 'verify-flow@example.ru')->first();
expect($user)->not->toBeNull();
expect($user->phone)->toBe('79123456789');
expect($user->email_verified_at)->not->toBeNull();
$this->assertAuthenticatedAs($user);
});
test('register/verify отклоняет неверный код и считает попытки', function () use ($startAndGetCode) {
$startAndGetCode();
$this->postJson('/api/auth/register/verify', ['code' => '000000'])
->assertStatus(422)->assertJsonValidationErrors(['code']);
expect(User::where('email', 'verify-flow@example.ru')->exists())->toBeFalse();
});
test('register/verify сбрасывает pending после 5 неверных попыток', function () use ($startAndGetCode) {
$startAndGetCode();
for ($i = 0; $i < 5; $i++) {
$this->postJson('/api/auth/register/verify', ['code' => '000000'])->assertStatus(422);
}
// 6-я попытка — pending уже сброшен (нет сессии регистрации).
$this->postJson('/api/auth/register/verify', ['code' => '000000'])
->assertStatus(422)->assertJsonValidationErrors(['code']);
});
test('register/verify без начатой регистрации возвращает 422', function () {
$this->postJson('/api/auth/register/verify', ['code' => '123456'])
->assertStatus(422)->assertJsonValidationErrors(['code']);
});
test('register/verify отклоняет истёкший код', function () use ($startAndGetCode) {
$code = $startAndGetCode();
$this->travel(16)->minutes();
$this->postJson('/api/auth/register/verify', ['code' => $code])
->assertStatus(422)->assertJsonValidationErrors(['code']);
expect(User::where('email', 'verify-flow@example.ru')->exists())->toBeFalse();
});
// ---------------------------------------------------------------------------
// Task 5: register/resend
// ---------------------------------------------------------------------------
test('register/resend в течение cooldown возвращает 429', function () use ($startAndGetCode) {
$startAndGetCode();
$this->postJson('/api/auth/register/resend')->assertStatus(429);
});
test('register/resend после cooldown шлёт новый код', function () use ($startAndGetCode) {
$startAndGetCode();
Mail::fake(); // сбрасываем счётчик отправок
$this->travel(61)->seconds();
$this->postJson('/api/auth/register/resend')->assertOk();
Mail::assertSent(RegisterEmailVerificationCode::class, 1);
});
test('register/resend без начатой регистрации возвращает 422', function () {
$this->postJson('/api/auth/register/resend')
->assertStatus(422)->assertJsonValidationErrors(['code']);
});