true]); }); function registerPayload(array $over = []): array { return array_merge([ 'email' => 'newclient@example.ru', 'password' => 'fresh-pass-123', 'accept_offer' => true, 'accept_pdn' => true, 'captcha_token' => 'tok-123', ], $over); } test('register создаёт pending-тенанта + неактивного владельца + код, без входа', function () { $r = $this->postJson('/api/auth/register', registerPayload()); $r->assertStatus(201); $r->assertJsonPath('status', 'pending_email_confirm'); $r->assertJsonPath('email', 'newclient@example.ru'); expect($r->json('_dev_plain_code'))->toMatch('/^\d{6}$/'); $user = User::where('email', 'newclient@example.ru')->first(); expect($user)->not->toBeNull(); expect($user->is_active)->toBeFalse(); $tenant = Tenant::find($user->tenant_id); expect($tenant->status)->toBe('pending_email_confirm'); expect((float) $tenant->balance_rub)->toBe(0.0); // Вход НЕ выполнен. $this->getJson('/api/auth/me')->assertStatus(401); }); test('register пишет email_verifications через pgsql_supplier (BYPASSRLS)', function () { $connections = []; DB::listen(function ($q) use (&$connections) { if (str_contains($q->sql, 'email_verifications')) { $connections[] = $q->connectionName; } }); $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); expect($connections)->not->toBeEmpty(); expect(array_values(array_unique($connections)))->toBe(['pgsql_supplier']); }); test('register отклоняет неверную капчу (422), аккаунт не создаётся', function () { config(['services.captcha.fake_passes' => false]); $this->postJson('/api/auth/register', registerPayload()) ->assertStatus(422) ->assertJsonValidationErrors(['captcha_token']); expect(User::where('email', 'newclient@example.ru')->exists())->toBeFalse(); }); test('register требует accept_offer, accept_pdn, captcha_token', function () { $this->postJson('/api/auth/register', registerPayload(['accept_offer' => false])) ->assertStatus(422)->assertJsonValidationErrors(['accept_offer']); $this->postJson('/api/auth/register', registerPayload(['accept_pdn' => false])) ->assertStatus(422)->assertJsonValidationErrors(['accept_pdn']); $payload = registerPayload(); unset($payload['captcha_token']); $this->postJson('/api/auth/register', $payload) ->assertStatus(422)->assertJsonValidationErrors(['captcha_token']); }); test('confirm верным кодом активирует тенанта/владельца, баланс 300, выполняет вход', function () { $reg = $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); $code = $reg->json('_dev_plain_code'); $r = $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $code, ]); $r->assertOk(); $r->assertJsonPath('user.email', 'newclient@example.ru'); $r->assertJsonPath('requires_2fa', false); $user = User::where('email', 'newclient@example.ru')->first(); expect($user->is_active)->toBeTrue(); $tenant = Tenant::find($user->tenant_id); expect($tenant->status)->toBe('active'); expect((float) $tenant->balance_rub)->toBe(300.0); }); test('confirm неверным кодом → 422 + failed_attempts++', function () { $reg = $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); $real = $reg->json('_dev_plain_code'); $wrong = $real === '000000' ? '111111' : '000000'; $r = $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $wrong, ]); $r->assertStatus(422); expect($r->json('attempts_remaining'))->toBe(4); }); test('5 неверных кодов инвалидируют запись (даже верный код больше не проходит)', function () { $reg = $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); $real = $reg->json('_dev_plain_code'); $wrong = $real === '000000' ? '111111' : '000000'; for ($i = 0; $i < 5; $i++) { $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $wrong, ])->assertStatus(422); } $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $real, ])->assertStatus(422)->assertJsonPath('reason', 'too_many_attempts'); }); test('протухший код отклоняется, resend выдаёт рабочий', function () { $reg = $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); $user = User::where('email', 'newclient@example.ru')->first(); DB::table('email_verifications')->where('user_id', $user->id) ->update(['expires_at' => now()->subMinutes(5)]); $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $reg->json('_dev_plain_code'), ])->assertStatus(422)->assertJsonPath('reason', 'expired'); $resend = $this->postJson('/api/auth/resend-code', ['email' => 'newclient@example.ru'])->assertOk(); $newCode = $resend->json('_dev_plain_code'); expect($newCode)->toMatch('/^\d{6}$/'); $this->postJson('/api/auth/confirm-email', [ 'email' => 'newclient@example.ru', 'code' => $newCode, ])->assertOk(); }); test('повторный register на неподтверждённый email не создаёт второго тенанта', function () { $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); $this->postJson('/api/auth/register', registerPayload())->assertStatus(201); expect(User::where('email', 'newclient@example.ru')->count())->toBe(1); expect(Tenant::where('contact_email', 'newclient@example.ru')->count())->toBe(1); }); test('register на активный email → 422', function () { $tenant = Tenant::factory()->create(); User::factory()->create([ 'tenant_id' => $tenant->id, 'email' => 'active@example.ru', 'is_active' => true, ]); $this->postJson('/api/auth/register', registerPayload(['email' => 'active@example.ru'])) ->assertStatus(422)->assertJsonValidationErrors(['email']); }); test('resend на несуществующий email → унифицированный ответ (anti-enumeration)', function () { $this->postJson('/api/auth/resend-code', ['email' => 'nobody@example.ru']) ->assertOk() ->assertJsonMissingPath('_dev_plain_code'); });