187 lines
7.2 KiB
PHP
187 lines
7.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Models\Tenant;
|
||
|
|
use App\Models\User;
|
||
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Tests\Concerns\SharesSupplierPdo;
|
||
|
|
|
||
|
|
// Самозапись пишет tenants/users/email_verifications через BYPASSRLS pgsql_supplier
|
||
|
|
// (на публичном роуте нет tenant-GUC). SharesSupplierPdo шарит PDO под DatabaseTransactions.
|
||
|
|
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||
|
|
|
||
|
|
beforeEach(function () {
|
||
|
|
config(['services.captcha.fake_passes' => 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');
|
||
|
|
});
|