2a34ee880a
- /api/dashboard/summary, /api/managers, /api/lead-statuses: были без auth (tenant_id параметром) → auth:sanctum (+tenant); tenant_id из authed-user, не из параметра — закрывает кросс-tenant утечку KPI/списка пользователей - ManagerController: явный where(tenant_id) поверх RLS (BYPASSRLS-роли/тесты) - WebhookUrlGuard + webhooks/test: SSRF-блок private/reserved/loopback IP (cloud-metadata 169.254.169.254 и пр.); update()/delivery — follow-up - TDD: +EndpointAuthHardeningTest(5) +WebhookSsrfGuardTest(10); обновлены Dashboard/Lookups/LeadStatuses тесты под auth - регрессия tests/Feature 960/964 (2 фейла pre-existing: Vite-manifest env + RouteSupplierLeadJobBilling idempotency — оба фейлят и на чистом base) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
86 lines
3.1 KiB
PHP
86 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Тесты GET /api/lead-statuses — глобальный lookup статусов воронки.
|
|
*
|
|
* Таблица lead_statuses не tenant-aware, seeded в schema.sql (5 системных
|
|
* статусов воронки: new/viewed/in_progress/won/lost). Go-live: эндпоинт за
|
|
* auth:sanctum (глобальная таблица — tenant-middleware не нужен).
|
|
*/
|
|
uses(DatabaseTransactions::class);
|
|
|
|
/** Авторизоваться любым пользователем (lead-statuses требует только auth:sanctum). */
|
|
function authLeadStatuses(): void
|
|
{
|
|
test()->actingAs(User::factory()->for(Tenant::factory())->create());
|
|
}
|
|
|
|
test('GET /api/lead-statuses без авторизации возвращает 401', function () {
|
|
$this->getJson('/api/lead-statuses')->assertStatus(401);
|
|
});
|
|
|
|
test('GET /api/lead-statuses возвращает 200 и не пустой список', function () {
|
|
authLeadStatuses();
|
|
$r = $this->getJson('/api/lead-statuses');
|
|
|
|
$r->assertStatus(200);
|
|
expect($r->json('lead_statuses'))->toBeArray();
|
|
expect(count($r->json('lead_statuses')))->toBeGreaterThanOrEqual(5);
|
|
});
|
|
|
|
test('GET /api/lead-statuses возвращает все 5 системных статусов из seed', function () {
|
|
authLeadStatuses();
|
|
$r = $this->getJson('/api/lead-statuses');
|
|
|
|
$slugs = collect($r->json('lead_statuses'))->pluck('slug')->all();
|
|
$expected = ['new', 'viewed', 'in_progress', 'won', 'lost'];
|
|
foreach ($expected as $slug) {
|
|
expect($slugs)->toContain($slug);
|
|
}
|
|
});
|
|
|
|
test('GET /api/lead-statuses возвращает поля slug, name_ru, color_hex, sort_order, is_system', function () {
|
|
authLeadStatuses();
|
|
$r = $this->getJson('/api/lead-statuses');
|
|
|
|
$first = $r->json('lead_statuses.0');
|
|
expect($first)->toHaveKeys(['slug', 'name_ru', 'is_system', 'sort_order', 'color_hex']);
|
|
expect($first['slug'])->toBeString();
|
|
expect($first['name_ru'])->toBeString();
|
|
expect($first['is_system'])->toBeBool();
|
|
});
|
|
|
|
test('GET /api/lead-statuses сортирует по sort_order', function () {
|
|
authLeadStatuses();
|
|
$r = $this->getJson('/api/lead-statuses');
|
|
|
|
$sortOrders = collect($r->json('lead_statuses'))->pluck('sort_order')->all();
|
|
$sorted = $sortOrders;
|
|
sort($sorted);
|
|
expect($sortOrders)->toBe($sorted);
|
|
});
|
|
|
|
test('GET /api/lead-statuses включает кастомный slug, добавленный после seed', function () {
|
|
authLeadStatuses();
|
|
DB::table('lead_statuses')->insert([
|
|
'slug' => 'custom_test_'.bin2hex(random_bytes(3)),
|
|
'name_ru' => 'Кастомный тест',
|
|
'is_system' => false,
|
|
'sort_order' => 999,
|
|
'color_hex' => '#ABCDEF',
|
|
]);
|
|
|
|
$r = $this->getJson('/api/lead-statuses');
|
|
|
|
$slugs = collect($r->json('lead_statuses'))->pluck('slug')->all();
|
|
$custom = collect($slugs)->first(fn ($s) => str_starts_with($s, 'custom_test_'));
|
|
expect($custom)->not->toBeNull();
|
|
});
|