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

143 lines
5.8 KiB
PHP
Raw Normal View History

<?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' => 'closed', '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' => 'closed']],
])->assertStatus(200);
expect($unknown->refresh()->mapped_to_slug)->toBe('closed')
->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);
});