143 lines
5.8 KiB
PHP
143 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\ImportLeadsJob;
|
||
use App\Models\ImportLog;
|
||
use App\Models\ImportUnknownStatus;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Queue;
|
||
use Illuminate\Support\Facades\Storage;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
|
||
beforeEach(function (): void {
|
||
$this->tenant = Tenant::factory()->create();
|
||
$this->user = User::factory()->for($this->tenant)->create();
|
||
// Устанавливаем контекст тенанта на уровне outer-транзакции DatabaseTransactions.
|
||
// Middleware SetTenantContext использует SET LOCAL внутри savepoint'а — без этой
|
||
// строки RLS-фильтрация активна только внутри HTTP-запроса, но прямые DB-запросы
|
||
// в тестах (count, factory) видят все тенанты. Паттерн из DealIndexTest.php.
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
$this->actingAs($this->user);
|
||
});
|
||
|
||
test('POST /api/imports принимает CSV, создаёт import_log, диспатчит job', function (): void {
|
||
Queue::fake();
|
||
Storage::fake('local');
|
||
|
||
$csv = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя'."\n".
|
||
'9001,Окна,окна,79161112233,2023/05/10 10:00:00,,,Новые,';
|
||
|
||
$response = $this->postJson('/api/imports', [
|
||
'file' => UploadedFile::fake()->createWithContent('leads.csv', $csv),
|
||
]);
|
||
|
||
$response->assertStatus(201)
|
||
->assertJsonPath('data.status', 'pending')
|
||
->assertJsonPath('data.filename', 'leads.csv');
|
||
|
||
Queue::assertPushed(ImportLeadsJob::class);
|
||
// Defense-in-depth: superuser на dev обходит RLS (BYPASSRLS), поэтому явно
|
||
// фильтруем по tenant_id — паттерн из DealIndexTest / DealController.
|
||
expect(ImportLog::query()->where('tenant_id', $this->tenant->id)->count())->toBe(1);
|
||
});
|
||
|
||
test('POST /api/imports отвергает не-CSV файл', function (): void {
|
||
Storage::fake('local');
|
||
|
||
$response = $this->postJson('/api/imports', [
|
||
'file' => UploadedFile::fake()->create('image.png', 10, 'image/png'),
|
||
]);
|
||
|
||
$response->assertStatus(422)->assertJsonValidationErrorFor('file');
|
||
});
|
||
|
||
test('POST /api/imports требует авторизации', function (): void {
|
||
app('auth')->forgetGuards();
|
||
|
||
$this->postJson('/api/imports', [])->assertStatus(401);
|
||
});
|
||
|
||
test('GET /api/imports возвращает только import_log своего тенанта', function (): void {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
ImportLog::factory()->count(2)->create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'user_id' => $this->user->id,
|
||
]);
|
||
|
||
$this->getJson('/api/imports')
|
||
->assertStatus(200)
|
||
->assertJsonCount(2, 'data');
|
||
});
|
||
|
||
test('GET /api/imports/{id} отдаёт прогресс', function (): void {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
$log = ImportLog::factory()->create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'user_id' => $this->user->id,
|
||
'status' => 'processing',
|
||
'rows_added' => 10,
|
||
]);
|
||
|
||
$this->getJson("/api/imports/{$log->id}")
|
||
->assertStatus(200)
|
||
->assertJsonPath('data.status', 'processing')
|
||
->assertJsonPath('data.rows_added', 10);
|
||
});
|
||
|
||
test('GET /api/imports/unknown-statuses возвращает незамапленные статусы', function (): void {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
ImportUnknownStatus::create([
|
||
'tenant_id' => $this->tenant->id, 'status_ru' => 'Архив', 'occurrences' => 3,
|
||
]);
|
||
ImportUnknownStatus::create([
|
||
'tenant_id' => $this->tenant->id, 'status_ru' => 'Спам', 'occurrences' => 1,
|
||
'mapped_to_slug' => 'lost', 'resolved_at' => now(),
|
||
]);
|
||
|
||
$this->getJson('/api/imports/unknown-statuses')
|
||
->assertStatus(200)
|
||
->assertJsonCount(1, 'data')
|
||
->assertJsonPath('data.0.status_ru', 'Архив');
|
||
});
|
||
|
||
test('POST /api/imports/unknown-statuses/resolve проставляет маппинг', function (): void {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
$unknown = ImportUnknownStatus::create([
|
||
'tenant_id' => $this->tenant->id, 'status_ru' => 'Архив', 'occurrences' => 3,
|
||
]);
|
||
|
||
$this->postJson('/api/imports/unknown-statuses/resolve', [
|
||
'mappings' => [['status_ru' => 'Архив', 'slug' => 'lost']],
|
||
])->assertStatus(200);
|
||
|
||
expect($unknown->refresh()->mapped_to_slug)->toBe('lost')
|
||
->and($unknown->resolved_at)->not->toBeNull();
|
||
});
|
||
|
||
test('resolve отвергает несуществующий slug', function (): void {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
|
||
$this->postJson('/api/imports/unknown-statuses/resolve', [
|
||
'mappings' => [['status_ru' => 'Архив', 'slug' => 'нет-такого']],
|
||
])->assertStatus(422);
|
||
});
|
||
|
||
test('GET /api/imports/{id} отвергает import_log чужого тенанта (403)', function (): void {
|
||
$otherTenant = Tenant::factory()->create();
|
||
$otherUser = User::factory()->for($otherTenant)->create();
|
||
$foreignLog = ImportLog::factory()->create([
|
||
'tenant_id' => $otherTenant->id,
|
||
'user_id' => $otherUser->id,
|
||
]);
|
||
|
||
// Авторизован пользователь $this->tenant; запрашиваем чужой import_log.
|
||
// abort_if(tenant_id mismatch, 403) в ImportController::show — defense-in-depth.
|
||
$this->getJson("/api/imports/{$foreignLog->id}")->assertStatus(403);
|
||
});
|