Files
portal/app/tests/Feature/Sales/SalesOwnershipScopeTest.php
T
Дмитрий 372668ad41 feat(sales): EnsureSalesUser + ScopesSalesOwnership
Task 0.4: middleware EnsureSalesUser (гейт зоны /api/sales — user('sales') активен, иначе 401, alias 'sales-portal'). Трейт ScopesSalesOwnership: менеджер видит только свои tenant_id из sales_client_assignments, начальник (head) — всех (null=без ограничения). Тест SalesOwnershipScopeTest 6/6, всего sales 18/18. Larastan чистый. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:43:31 +03:00

188 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Http\Controllers\Concerns\ScopesSalesOwnership;
use App\Models\SalesClientAssignment;
use App\Models\SalesUser;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Hash;
/**
* TDD: трейт ScopesSalesOwnership.
*
* Проверяет логику ограничения ownership:
* - Менеджер видит только своих клиентов (tenant_ids из sales_client_assignments).
* - Начальник (role=head) видит всех — ограничение не применяется (null).
*
* Изоляция: DatabaseTransactions — каждый тест откатывается.
* Используем DEFAULT connection (pgsql → liderra_testing).
*
* Spec: docs/superpowers/plans/2026-06-30-sales-portal.md (Task 0.4)
*/
uses(DatabaseTransactions::class);
/**
* Зонд для тестирования трейта без реального контроллера.
*/
class OwnershipProbe
{
use ScopesSalesOwnership;
public function __construct(private SalesUser $u) {}
/** @return list<int>|null */
public function ids(): ?array
{
return $this->ownedTenantIds($this->u);
}
public function scoped(Builder $query): Builder
{
return $this->scopeByOwnership($query, $this->u);
}
}
// ── helpers ────────────────────────────────────────────────────────────────
function makeSalesUserOwn(string $role = 'manager'): SalesUser
{
return SalesUser::create([
'name' => ucfirst($role).' '.uniqid(),
'email' => $role.uniqid().'@scope.local',
'password' => Hash::make('secret'),
'role' => $role,
]);
}
// ── 1. ownedTenantIds ──────────────────────────────────────────────────────
test('ownedTenantIds для менеджера возвращает список его tenant_id', function () {
$manager = makeSalesUserOwn('manager');
$t1 = Tenant::factory()->create();
$t2 = Tenant::factory()->create();
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t1->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t2->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
$probe = new OwnershipProbe($manager);
$ids = $probe->ids();
expect($ids)->toBeArray()
->toEqualCanonicalizing([$t1->id, $t2->id]);
});
test('ownedTenantIds для head возвращает null (нет ограничения)', function () {
$head = makeSalesUserOwn('head');
$probe = new OwnershipProbe($head);
expect($probe->ids())->toBeNull();
});
test('ownedTenantIds для менеджера без клиентов возвращает пустой массив', function () {
$manager = makeSalesUserOwn('manager');
$probe = new OwnershipProbe($manager);
$ids = $probe->ids();
expect($ids)->toBeArray()->toBeEmpty();
});
// ── 2. scopeByOwnership ────────────────────────────────────────────────────
test('scopeByOwnership для менеджера ограничивает запрос его tenant_id', function () {
$manager = makeSalesUserOwn('manager');
$other = makeSalesUserOwn('manager');
$t1 = Tenant::factory()->create();
$t2 = Tenant::factory()->create();
$t3 = Tenant::factory()->create();
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t1->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t2->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
// t3 принадлежит другому менеджеру
SalesClientAssignment::create([
'sales_user_id' => $other->id,
'tenant_id' => $t3->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
$probe = new OwnershipProbe($manager);
$results = $probe->scoped(SalesClientAssignment::query())->get();
$tenantIds = $results->pluck('tenant_id')->sort()->values()->all();
expect($tenantIds)->toEqualCanonicalizing([$t1->id, $t2->id]);
});
test('scopeByOwnership для head не ограничивает запрос (видит всех)', function () {
$head = makeSalesUserOwn('head');
$manager = makeSalesUserOwn('manager');
$t1 = Tenant::factory()->create();
$t2 = Tenant::factory()->create();
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t1->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $t2->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
$probe = new OwnershipProbe($head);
$results = $probe->scoped(SalesClientAssignment::query())->get();
// Head видит все записи (в контексте транзакции — минимум 2 наши)
expect($results->count())->toBeGreaterThanOrEqual(2);
});
test('scopeByOwnership для менеджера без клиентов возвращает пустую коллекцию', function () {
$manager = makeSalesUserOwn('manager');
// Создаём другого менеджера с клиентом, чтобы таблица не была пустой
$other = makeSalesUserOwn('manager');
$t1 = Tenant::factory()->create();
SalesClientAssignment::create([
'sales_user_id' => $other->id,
'tenant_id' => $t1->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
$probe = new OwnershipProbe($manager);
$results = $probe->scoped(SalesClientAssignment::query())->get();
expect($results)->toBeEmpty();
});