Files
portal/app/tests/Feature/Import/ImportControllerTest.php
T

143 lines
5.8 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\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);
});