Files
portal/app/tests/Feature/LookupsTest.php
T
Дмитрий b5849bbd2a fix(projects): cyrillic ILIKE via PG ICU + clearable workaround
Корень: dev-БД `liderra` создавалась с LC_CTYPE=C — lower()/upper() не
делает case-folding для кириллицы, `ILIKE '%сп%'` на «Окна СПб» = 0 строк.
Test-БД с Russian_Russia.1251 маскировала проблему.

Системный fix: dev-БД пересоздана через `LOCALE_PROVIDER icu ICU_LOCALE 'und'`
(PG 16+ ICU collation, кросс-платформенно). Точечный COLLATE-workaround не
понадобился — все 5 ILIKE-endpoint'ов теперь работают с кириллицей без
правки кода. CTO-20 закрыт в реестре v1.81; команда CREATE DATABASE с ICU
зафиксирована для prod-deploy.

Сопутствующее:
- ProjectsView clearable: workaround `::after content '✕'` + видимость
  через `.v-field--dirty` (mdi-* font не подключён в проекте — CTO-19
  заведён в реестре).
- LookupsTest: удалён stale case `GET /api/projects?tenant_id=N`,
  заменённый auth:sanctum-роутом в Plan 5.
- Pest +1 регрессионный тест (`search is case-insensitive for Cyrillic`)
  в ProjectsListShowTest, 10/10 / 37 assertions.
- phpstan-baseline регенерирован (3 actingAs + удалённый case).
- cspell-words: +Регистронезависимый, +und.
- app/.backups/ в gitignore.

Verify:
- Pest --parallel: 742 passed / 1 flaky error (CsvReconcileJobTest cache
  race, в изоляции 2/2 PASS) / 3 skipped.
- Browser: «сп» и «окн» возвращают «Окна СПб».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:25:25 +03:00

104 lines
3.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
});
test('GET /api/managers возвращает active users тенанта', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
User::factory()->for($this->tenant)->create([
'first_name' => 'Иван', 'last_name' => 'Петров', 'is_active' => true,
]);
User::factory()->for($this->tenant)->create([
'first_name' => 'Ольга', 'last_name' => 'Романова', 'is_active' => true,
]);
User::factory()->for($this->tenant)->create([
'first_name' => 'Удалённый', 'is_active' => false,
]);
$r = $this->getJson('/api/managers?tenant_id='.$this->tenant->id);
$r->assertStatus(200);
$managers = $r->json('managers');
expect($managers)->toHaveCount(2);
$names = array_column($managers, 'name');
expect($names)->toContain('Иван П.')->toContain('Ольга Р.');
});
test('GET /api/managers возвращает initials с fallback на email', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
User::factory()->for($this->tenant)->create([
'email' => 'admin@example.ru',
'first_name' => null,
'last_name' => null,
'is_active' => true,
]);
$r = $this->getJson('/api/managers?tenant_id='.$this->tenant->id);
$r->assertStatus(200);
$manager = $r->json('managers.0');
expect($manager['name'])->toBe('admin@example.ru');
expect($manager['initials'])->toBe('AD');
});
test('GET /api/managers 422 без tenant_id', function () {
$r = $this->getJson('/api/managers');
$r->assertStatus(422);
});
test('GET /api/managers 404 unknown tenant', function () {
$r = $this->getJson('/api/managers?tenant_id=999999');
$r->assertStatus(404);
});
test('POST /api/deals 422 если manager_id не принадлежит tenant\'у', function () {
$otherTenant = Tenant::factory()->create();
DB::statement('SET app.current_tenant_id = '.$otherTenant->id);
$otherManager = User::factory()->for($otherTenant)->create(['is_active' => true]);
// Назначаем чужого менеджера на свою сделку — должен быть 422.
$r = $this->postJson('/api/deals', [
'tenant_id' => $this->tenant->id,
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
'manager_id' => $otherManager->id,
]);
$r->assertStatus(422);
expect($r->json('errors'))->toHaveKey('manager_id');
});
test('POST /api/deals 422 если manager_id не активен (is_active=false)', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$inactive = User::factory()->for($this->tenant)->create(['is_active' => false]);
$r = $this->postJson('/api/deals', [
'tenant_id' => $this->tenant->id,
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
'manager_id' => $inactive->id,
]);
$r->assertStatus(422);
});
test('POST /api/deals принимает manager_id из своего tenant\'а', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$manager = User::factory()->for($this->tenant)->create(['is_active' => true]);
$r = $this->postJson('/api/deals', [
'tenant_id' => $this->tenant->id,
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
'manager_id' => $manager->id,
]);
$r->assertStatus(201);
expect($r->json('deal.manager_id'))->toBe($manager->id);
});