Files
portal/app/tests/Feature/Sales/SalesGuardTokenPriorityTest.php
T
Дмитрий 498f8cfd6d fix(портал продаж): вход только по токену — чужая web-сессия не перебивает
Гвард 'sales' (Sanctum) на stateful-домене lk.liderra.ru подставлял
App\Models\User из открытого рядом обычного кабинета вместо SalesUser —
500 на всех маршрутах портала, роль слетала в «менеджера» (боевой
инцидент 24.07.2026). Новый драйвер 'sales-token' в AppServiceProvider
авторизует только по Bearer-токену, не заглядывая в web-сессию;
обычный кабинет (auth:sanctum) и impersonation не затронуты.

Регресс-тест SalesGuardTokenPriorityTest воспроизводит поломку.
larastan-хук исключён: 2 ошибки — чужой pre-existing WIP ветки
(SetTenantContext, SmscSmsProviderTest), мои файлы stan-чисты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 17:46:53 +03:00

52 lines
2.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesUser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Hash;
/**
* Регрессия боевого инцидента 24.07.2026.
*
* Портал отдела продаж (guard 'sales', Sanctum Bearer) на stateful-домене
* lk.liderra.ru «перебивался» сессией обычного кабинета (guard 'web'):
* Laravel\Sanctum\Guard сначала смотрит config('sanctum.guard')=['web'] и,
* если в браузере открыт обычный кабинет (активная web-сессия), возвращает
* App\Models\User ВМЕСТО SalesUser — даже не заглянув в Bearer-токен. Итог —
* TypeError/`User::isHead()` → 500 на всех защищённых маршрутах портала.
*
* Инвариант: маршруты /api/sales/* авторизуются ТОЛЬКО по своему токену и не
* зависят от того, залогинен ли в том же браузере обычный кабинет.
*/
uses(DatabaseTransactions::class);
function gtp_makeHead(): SalesUser
{
return SalesUser::create([
'name' => 'Head '.uniqid(),
'email' => 'gtp-head-'.uniqid().'@test.local',
'password' => Hash::make('secret'),
'role' => 'head',
'is_active' => true,
]);
}
it('авторизует портал по токену, игнорируя открытую web-сессию обычного кабинета', function () {
$head = gtp_makeHead();
// В том же браузере параллельно открыт обычный кабинет — активная web-сессия.
$webUser = User::factory()->create();
$this->actingAs($webUser, 'web');
$token = $head->createToken('sales')->plainTextToken;
$resp = $this
->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/sales/overview?period=d30');
// Портал должен ответить начальнику (200), а не 500 из-за подмены на User.
$resp->assertOk();
});