Files
portal/app/tests/Feature/DealIndexTest.php
T

229 lines
9.6 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\Deal;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
/**
* Тесты GET /api/deals — list-endpoint для DealsView/KanbanView (замена MOCK_DEALS).
*
* Покрывает: фильтры (status_in, project_id, manager_id, search), сортировку
* по received_at DESC, RLS-изоляцию между tenant'ами, относительные поля
* (project_name, manager_name/initials), 422/404, пагинацию (limit/offset).
*/
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->otherTenant = Tenant::factory()->create();
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$this->project = Project::factory()->for($this->tenant)->create(['name' => 'Окна Москва']);
$this->project2 = Project::factory()->for($this->tenant)->create(['name' => 'Натяжные потолки']);
$this->manager = User::factory()->for($this->tenant)->create([
'first_name' => 'Иван',
'last_name' => 'Петров',
'email' => 'ivan@example.test',
]);
});
test('GET /api/deals возвращает 422 без tenant_id', function () {
$this->getJson('/api/deals')->assertStatus(422);
});
test('GET /api/deals возвращает 404 для unknown tenant_id', function () {
$this->getJson('/api/deals?tenant_id=999999')->assertStatus(404);
});
test('GET /api/deals возвращает пустой список для tenant без сделок', function () {
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
$r->assertStatus(200)
->assertJson(['deals' => [], 'total' => 0, 'limit' => 100, 'offset' => 0]);
});
test('GET /api/deals возвращает сделки tenant\'а с проектом и менеджером', function () {
$deal = Deal::factory()
->for($this->tenant)
->for($this->project)
->create([
'phone' => '+7 (999) 111-11-11',
'contact_name' => 'Анна С.',
'status' => 'new',
'manager_id' => $this->manager->id,
]);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
$r->assertStatus(200);
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.id'))->toBe($deal->id);
expect($r->json('deals.0.phone'))->toBe('+7 (999) 111-11-11');
expect($r->json('deals.0.project_name'))->toBe('Окна Москва');
expect($r->json('deals.0.manager_name'))->toBe('Иван П.');
expect($r->json('deals.0.manager_initials'))->toBe('ИП');
expect($r->json('deals.0.contact_name'))->toBe('Анна С.');
expect($r->json('deals.0.received_at'))->toBeString();
});
test('GET /api/deals не возвращает сделки чужого tenant\'а (RLS)', function () {
Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
$foreignProject = Project::factory()->for($this->otherTenant)->create();
Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(['status' => 'new']);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.tenant_id'))->toBe($this->tenant->id);
});
test('GET /api/deals сортирует по received_at DESC', function () {
$oldest = Deal::factory()->for($this->tenant)->for($this->project)->create([
'received_at' => now()->subHours(3),
]);
$newest = Deal::factory()->for($this->tenant)->for($this->project)->create([
'received_at' => now()->subMinutes(1),
]);
$middle = Deal::factory()->for($this->tenant)->for($this->project)->create([
'received_at' => now()->subHours(1),
]);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
expect($r->json('deals.0.id'))->toBe($newest->id);
expect($r->json('deals.1.id'))->toBe($middle->id);
expect($r->json('deals.2.id'))->toBe($oldest->id);
});
test('GET /api/deals фильтрует по status_in[]', function () {
Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'paid']);
Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'closed']);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&status_in[]=new&status_in[]=paid');
expect($r->json('total'))->toBe(2);
$statuses = collect($r->json('deals'))->pluck('status')->sort()->values()->all();
expect($statuses)->toBe(['new', 'paid']);
});
test('GET /api/deals фильтрует по project_id', function () {
Deal::factory()->for($this->tenant)->for($this->project)->create();
Deal::factory()->for($this->tenant)->for($this->project)->create();
Deal::factory()->for($this->tenant)->for($this->project2)->create();
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&project_id='.$this->project2->id);
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.project_name'))->toBe('Натяжные потолки');
});
test('GET /api/deals фильтрует по manager_id', function () {
$other = User::factory()->for($this->tenant)->create();
Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $this->manager->id]);
Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $other->id]);
Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&manager_id='.$this->manager->id);
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.manager_id'))->toBe($this->manager->id);
});
test('GET /api/deals фильтрует по search (phone + contact_name, ILIKE)', function () {
Deal::factory()->for($this->tenant)->for($this->project)->create([
'phone' => '+7 (999) 111-11-11',
'contact_name' => 'Анна Соколова',
]);
Deal::factory()->for($this->tenant)->for($this->project)->create([
'phone' => '+7 (903) 222-22-22',
'contact_name' => 'Дмитрий Петров',
]);
expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=Соколова')
->json('total'))->toBe(1);
expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=903')
->json('total'))->toBe(1);
expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=сокол') // case-insensitive ILIKE
->json('total'))->toBe(1);
});
test('GET /api/deals поддерживает limit + offset', function () {
foreach (range(1, 5) as $i) {
Deal::factory()->for($this->tenant)->for($this->project)->create([
'received_at' => now()->subMinutes($i),
]);
}
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&limit=2&offset=1');
expect($r->json('total'))->toBe(5);
expect($r->json('limit'))->toBe(2);
expect($r->json('offset'))->toBe(1);
expect(count($r->json('deals')))->toBe(2);
});
test('GET /api/deals?only_deleted=true возвращает только soft-deleted', function () {
$alive = Deal::factory()->for($this->tenant)->for($this->project)->create();
$deleted1 = Deal::factory()->for($this->tenant)->for($this->project)->create();
$deleted2 = Deal::factory()->for($this->tenant)->for($this->project)->create();
$deleted1->delete();
$deleted2->delete();
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&only_deleted=true');
expect($r->json('total'))->toBe(2);
$ids = collect($r->json('deals'))->pluck('id')->all();
expect($ids)->not->toContain($alive->id);
expect($ids)->toContain($deleted1->id);
expect($ids)->toContain($deleted2->id);
});
test('GET /api/deals (без only_deleted) НЕ возвращает soft-deleted (default)', function () {
$alive = Deal::factory()->for($this->tenant)->for($this->project)->create();
$deleted = Deal::factory()->for($this->tenant)->for($this->project)->create();
$deleted->delete();
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.id'))->toBe($alive->id);
});
test('GET /api/deals?only_deleted=true изолирует чужие удалённые сделки', function () {
DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
$foreignProject = Project::factory()->for($this->otherTenant)->create();
$foreign = Deal::factory()->for($this->otherTenant)->for($foreignProject)->create();
$foreign->delete();
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$own = Deal::factory()->for($this->tenant)->for($this->project)->create();
$own->delete();
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&only_deleted=true');
expect($r->json('total'))->toBe(1);
expect($r->json('deals.0.id'))->toBe($own->id);
});
test('GET /api/deals возвращает manager_name/initials = null если manager_id null', function () {
Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]);
$r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id);
expect($r->json('deals.0.manager_id'))->toBeNull();
expect($r->json('deals.0.manager_name'))->toBeNull();
expect($r->json('deals.0.manager_initials'))->toBeNull();
});