Files
portal/app/tests/Feature/Auth/AuthControllerTest.php
T
Дмитрий 04b90afda4 phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
  personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
  + кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
  accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
  маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
  middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.

Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
  validation + last_login_at update + register success/duplicate/без accept +
  me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
  запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).

Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.

CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.

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

214 lines
7.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Hash;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
});
test('POST /api/auth/login возвращает user + requires_2fa=false для обычного user', function () {
$user = User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'login-test@example.ru',
'password_hash' => Hash::make('secret-pass-123'),
'totp_enabled' => false,
'is_active' => true,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'login-test@example.ru',
'password' => 'secret-pass-123',
]);
$response->assertOk();
$response->assertJsonPath('user.id', $user->id);
$response->assertJsonPath('user.email', 'login-test@example.ru');
$response->assertJsonPath('requires_2fa', false);
expect($response->json('user'))->not->toHaveKey('password_hash');
});
test('POST /api/auth/login c totp_enabled=true возвращает requires_2fa=true', function () {
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'totp-user@example.ru',
'password_hash' => Hash::make('secret-pass-123'),
'totp_enabled' => true,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'totp-user@example.ru',
'password' => 'secret-pass-123',
]);
$response->assertOk();
$response->assertJsonPath('requires_2fa', true);
});
test('POST /api/auth/login возвращает 422 при неверном пароле', function () {
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'wrong-pass@example.ru',
'password_hash' => Hash::make('right-pass-123'),
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'wrong-pass@example.ru',
'password' => 'wrong-pass-456',
]);
$response->assertStatus(422);
$response->assertJsonPath('errors.email.0', 'Неверный email или пароль.');
});
test('POST /api/auth/login возвращает 422 для несуществующего email', function () {
$response = $this->postJson('/api/auth/login', [
'email' => 'nonexistent@example.ru',
'password' => 'any-password',
]);
$response->assertStatus(422);
});
test('POST /api/auth/login возвращает 422 для заблокированного аккаунта (is_active=false)', function () {
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'blocked@example.ru',
'password_hash' => Hash::make('right-pass-123'),
'is_active' => false,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'blocked@example.ru',
'password' => 'right-pass-123',
]);
$response->assertStatus(422);
$response->assertJsonPath('errors.email.0', 'Аккаунт заблокирован.');
});
test('POST /api/auth/login валидирует email format и password min:8', function () {
$response = $this->postJson('/api/auth/login', [
'email' => 'not-an-email',
'password' => 'short',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['email', 'password']);
});
test('POST /api/auth/login обновляет last_login_at у user', function () {
$user = User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'lastlogin@example.ru',
'password_hash' => Hash::make('secret-pass-123'),
'last_login_at' => null,
]);
$this->postJson('/api/auth/login', [
'email' => 'lastlogin@example.ru',
'password' => 'secret-pass-123',
])->assertOk();
expect($user->fresh()->last_login_at)->not->toBeNull();
});
test('POST /api/auth/register создаёт user + возвращает 201', function () {
$response = $this->postJson('/api/auth/register', [
'email' => 'new-signup@example.ru',
'password' => 'fresh-pass-123',
'accept_offer' => true,
'accept_pdn' => true,
]);
$response->assertStatus(201);
$response->assertJsonPath('user.email', 'new-signup@example.ru');
$response->assertJsonPath('requires_2fa', false);
$user = User::where('email', 'new-signup@example.ru')->first();
expect($user)->not->toBeNull();
expect(Hash::check('fresh-pass-123', $user->password_hash))->toBeTrue();
});
test('POST /api/auth/register отвергает существующий email (unique)', function () {
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'duplicate@example.ru',
]);
$response = $this->postJson('/api/auth/register', [
'email' => 'duplicate@example.ru',
'password' => 'any-password-123',
'accept_offer' => true,
'accept_pdn' => true,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['email']);
});
test('POST /api/auth/register требует accept_offer=true И accept_pdn=true (ТЗ §1.5/§4.1)', function () {
$base = [
'email' => 'no-consent@example.ru',
'password' => 'fresh-pass-123',
];
// Без оферты.
$this->postJson('/api/auth/register', array_merge($base, ['accept_pdn' => true]))
->assertStatus(422)
->assertJsonValidationErrors(['accept_offer']);
// Без ПДн.
$this->postJson('/api/auth/register', array_merge($base, ['accept_offer' => true]))
->assertStatus(422)
->assertJsonValidationErrors(['accept_pdn']);
});
test('GET /api/auth/me возвращает 401 без авторизации', function () {
$this->getJson('/api/auth/me')->assertStatus(401);
});
test('GET /api/auth/me возвращает user после login', function () {
$user = User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'me-test@example.ru',
'password_hash' => Hash::make('secret-pass-123'),
]);
$this->postJson('/api/auth/login', [
'email' => 'me-test@example.ru',
'password' => 'secret-pass-123',
])->assertOk();
$this->getJson('/api/auth/me')
->assertOk()
->assertJsonPath('user.id', $user->id)
->assertJsonPath('user.email', 'me-test@example.ru');
});
test('POST /api/auth/logout успешно завершает сессию (200 + flash-message)', function () {
User::factory()->create([
'tenant_id' => $this->tenant->id,
'email' => 'logout-test@example.ru',
'password_hash' => Hash::make('secret-pass-123'),
]);
$this->postJson('/api/auth/login', [
'email' => 'logout-test@example.ru',
'password' => 'secret-pass-123',
])->assertOk();
// logout возвращает 200 с message. Полное invalidate-session тестирование
// проблемно в Pest-runtime (cookie-jar держит session между запросами теста);
// это тестируется через Pest browser-mode — отдельный коммит.
$this->postJson('/api/auth/logout')
->assertOk()
->assertJsonPath('message', 'Вы вышли из системы.');
});