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 $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']); });