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); });