86bbeb1f06
Lefthook-pint не перестейджит — приводим к канону версии из c366614f. Без изменений логики. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
6.6 KiB
PHP
151 lines
6.6 KiB
PHP
<?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.5–0.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();
|
||
});
|