Files
portal/app/tests/Feature/Sales/SalesAuthTest.php
T
Дмитрий 86bbeb1f06 style(sales): pint-канон контроллера/маршрутов/теста входа
Lefthook-pint не перестейджит — приводим к канону версии из c366614f. Без изменений логики. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:09:05 +03:00

151 lines
6.6 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\SalesUser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Tests\Concerns\SharesSupplierPdo;
/**
* TDD: SalesAuthController — login / me / logout.
*
* Покрывает Task 0.5 + 0.6 портала продаж:
* - POST /api/sales/auth/login → 200 (token + user) / 422 (неверный логин) / 403 (отключён)
* - GET /api/sales/auth/me → 401 без токена / 200 с токеном
* - POST /api/sales/auth/logout → 200; повторный GET /me → 401
*
* Маршруты идут через middleware admin-db (UseAdminConnection), который
* переключает default-соединение на pgsql_admin (crm_admin_user). В тестах
* SharesAdminPdo (глобально в Pest.php) связывает pgsql и pgsql_admin через
* один PDO, чтобы данные, засеянные через pgsql, были видны запросу.
*
* Spec: docs/superpowers/plans/2026-06-30-sales-portal.md (Tasks 0.50.6)
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// ─── helpers ────────────────────────────────────────────────────────────────
/**
* Создаёт SalesUser с известным паролем и возвращает [user, plainPassword].
*
* @return array{0: SalesUser, 1: string}
*/
function makeSalesUserWithPassword(bool $active = true): array
{
$plain = 'SecretPass-'.uniqid();
$user = SalesUser::create([
'name' => 'Тест Менеджер '.uniqid(),
'email' => 'salesauth-'.uniqid().'@test.local',
'password' => Hash::make($plain),
'role' => 'manager',
'is_active' => $active,
]);
return [$user, $plain];
}
// ─── login ───────────────────────────────────────────────────────────────────
test('login: верные данные активного пользователя → 200, token и user.role', function () {
[$user, $plain] = makeSalesUserWithPassword();
$response = $this->postJson('/api/sales/auth/login', [
'email' => $user->email,
'password' => $plain,
]);
$response->assertOk();
expect($response->json('token'))->not->toBeNull()->not->toBe('');
expect($response->json('user.role'))->toBe('manager');
expect($response->json('user.email'))->toBe($user->email);
});
test('login: неверный пароль → 422 с сообщением', function () {
[$user] = makeSalesUserWithPassword();
$response = $this->postJson('/api/sales/auth/login', [
'email' => $user->email,
'password' => 'wrong-password-xyz',
]);
$response->assertStatus(422);
expect($response->json('message'))->toBe('Неверный логин или пароль.');
});
test('login: несуществующий email → 422 с сообщением', function () {
$response = $this->postJson('/api/sales/auth/login', [
'email' => 'nonexistent-'.uniqid().'@test.local',
'password' => 'some-password',
]);
$response->assertStatus(422);
expect($response->json('message'))->toBe('Неверный логин или пароль.');
});
test('login: неактивный пользователь с верным паролем → 403', function () {
[$user, $plain] = makeSalesUserWithPassword(active: false);
$response = $this->postJson('/api/sales/auth/login', [
'email' => $user->email,
'password' => $plain,
]);
$response->assertStatus(403);
expect($response->json('message'))->toBe('Аккаунт отключён, обратитесь к начальнику.');
});
// ─── me ──────────────────────────────────────────────────────────────────────
test('me: запрос без токена → 401', function () {
$this->getJson('/api/sales/auth/me')
->assertUnauthorized();
});
test('me: запрос с валидным Bearer-токеном → 200 с данными пользователя', function () {
[$user, $plain] = makeSalesUserWithPassword();
// Получаем токен через логин.
$loginResponse = $this->postJson('/api/sales/auth/login', [
'email' => $user->email,
'password' => $plain,
]);
$loginResponse->assertOk();
$token = $loginResponse->json('token');
// Запрашиваем /me с токеном.
$meResponse = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/sales/auth/me');
$meResponse->assertOk();
expect($meResponse->json('email'))->toBe($user->email);
expect($meResponse->json('role'))->toBe('manager');
});
// ─── logout ──────────────────────────────────────────────────────────────────
test('logout: выход инвалидирует токен, повторный /me → 401', function () {
[$user, $plain] = makeSalesUserWithPassword();
// Создаём токен напрямую (без login endpoint) — стабильнее в тестах.
// Проверка самого login-эндпоинта покрыта первым тестом выше.
$plainToken = $user->createToken('sales')->plainTextToken;
// Выход через endpoint.
$logoutResponse = $this->withHeader('Authorization', 'Bearer '.$plainToken)
->postJson('/api/sales/auth/logout');
$logoutResponse->assertOk();
expect($logoutResponse->json('message'))->toBe('Вы вышли.');
// Сбрасываем кэш guard-инстансов в AuthManager (общий singleton в контейнере).
// Без этого guard('sales') возвращает пользователя, кэшированного из предыдущего
// logout-запроса, даже если токен уже удалён из БД.
Auth::forgetGuards();
// Повторный /me с тем же токеном должен вернуть 401.
$this->withHeader('Authorization', 'Bearer '.$plainToken)
->getJson('/api/sales/auth/me')
->assertUnauthorized();
});