Files
portal/app/tests/Feature/Advertising/AdvertisingCampaignPhonesUploadTest.php
T
Дмитрий 2590c01477 test: половина грязи рекламных тестов убрана — остаток в базе со 116 до 44
Восьми файлам рекламной папки поставлен откат после теста. Проверено остатком
в базе снаружи: папка оставляла 116 записей арендаторов, теперь 44. Папка
целиком зелёная - 351 из 351. Полный прогон после уборки: 4111 тестов,
4107 зелёных, 4 пропущено, ноль падений - значит на эту грязь никто не
опирался. Побочно прогон стал БЫСТРЕЕ: 12,9 минуты против 16,5.

Один файл намеренно оставлен как был, причина записана прямо в нём:
CampaignBannerEndpointsTest нарочно ловит отказ базы, а Postgres после отказа
внутри транзакции глушит все следующие команды. С откатом файл падает -
чинить надо сам тест, а не обёртку.

Датчик пришлось расшифровывать, и это стоит помнить. Числа сперва выходили
с минусами: файл, который базы не касается вовсе, «уносил» девять записей.
Прямым замером выяснилось, что КАЖДЫЙ прогон начинается с пересборки базы -
было 6 записей, стало 0 после теста про расписание. Значит разница «до/после»
при переборе файлов = оставил этот минус оставил предыдущий. Расшифровал
цепочкой, сумма сошлась со 116 ровно - на этом расшифровка и проверяется.
Поимённый расклад по файлам перенесён в промт.

Грабля на будущее, тоже в промте: uses() обязан стоять НИЖЕ подключений
классов. Подключение действует с места объявления и ниже, поэтому вызов
выше падает «класс не найден» - и падает сразу вся папка. Полное имя
с ведущей чертой не спасает: pint САМ превращает его в подключение внизу
и ломает работавший файл.

Остаток долга измерен и НЕ закрыт: ~20 файлов оставляют по одной-две записи,
среди них миграционные, которым откат может быть противопоказан по сути.
По всей папке Feature без отката 96 файлов из 580 - корень общий,
в app/tests/Pest.php строка RefreshDatabase закомментирована.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:46:14 +03:00

174 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AdCampaign;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
// Откат после теста. Без него записи файла остаются в базе и роняют ЧУЖИЕ тесты:
// 01.08.2026 девятнадцать чужих проводок уронили денежную проверку приёмника МТС.
uses(DatabaseTransactions::class);
/**
* T17 — загрузка «моего списка» номеров: POST /api/advertising/campaigns/{id}/phones.
*
* Фикстуры — только синтетические номера (+7 900 000-00-0X / 7900000000X), не ПДн.
*/
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
$this->campaign = AdCampaign::create([
'tenant_id' => $this->tenant->id,
'name' => 'Кампания под список номеров',
'audience_days' => 10,
'weekly_budget_rub' => '1000.00',
]);
});
it('recognizes valid phones from text, dedupes and skips invalid ones', function () {
$text = implode("\n", [
'+7 900 000-00-01',
'79000000002',
'89000000003',
'9000000004', // 10 digits without leading 7/8 — valid RU mobile
'79000000002', // duplicate of line 2
'not-a-phone', // invalid
'123', // invalid
]);
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'text' => $text,
]);
$response->assertOk()
->assertJsonPath('recognized', 4)
->assertJsonPath('skipped', 3);
$this->assertDatabaseHas('ad_campaign_phones', [
'tenant_id' => $this->tenant->id,
'campaign_id' => $this->campaign->id,
'phone' => '79000000001',
]);
$this->assertDatabaseHas('ad_campaign_phones', [
'campaign_id' => $this->campaign->id,
'phone' => '79000000002',
]);
$this->assertDatabaseHas('ad_campaign_phones', [
'campaign_id' => $this->campaign->id,
'phone' => '79000000003',
]);
$this->assertDatabaseHas('ad_campaign_phones', [
'campaign_id' => $this->campaign->id,
'phone' => '79000000004',
]);
$count = DB::table('ad_campaign_phones')->where('campaign_id', $this->campaign->id)->count();
expect($count)->toBe(4);
});
it('recognizes valid phones from an uploaded csv file', function () {
$content = implode("\n", [
'+7 900 000-00-05',
'79000000006',
'garbage',
]);
$file = UploadedFile::fake()->createWithContent('phones.csv', $content);
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'file' => $file,
]);
$response->assertOk()
->assertJsonPath('recognized', 2)
->assertJsonPath('skipped', 1);
$this->assertDatabaseHas('ad_campaign_phones', [
'campaign_id' => $this->campaign->id,
'phone' => '79000000005',
]);
$this->assertDatabaseHas('ad_campaign_phones', [
'campaign_id' => $this->campaign->id,
'phone' => '79000000006',
]);
});
it('does not fail on repeated upload thanks to the unique constraint', function () {
$this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'text' => '79000000007',
])->assertOk();
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'text' => '79000000007',
]);
$response->assertOk()->assertJsonPath('recognized', 1);
$count = DB::table('ad_campaign_phones')
->where('campaign_id', $this->campaign->id)
->where('phone', '79000000007')
->count();
expect($count)->toBe(1);
});
it('returns 422 when neither file nor text is provided ("тихий ноль")', function () {
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", []);
$response->assertStatus(422)
->assertJsonPath('message', 'Загрузите файл или вставьте номера');
$count = DB::table('ad_campaign_phones')->where('campaign_id', $this->campaign->id)->count();
expect($count)->toBe(0);
});
it('returns 422 when text is only whitespace and no file is provided', function () {
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'text' => ' ',
]);
$response->assertStatus(422)
->assertJsonPath('message', 'Загрузите файл или вставьте номера');
});
it('rejects a file with a disallowed extension', function () {
$file = UploadedFile::fake()->createWithContent('phones.pdf', '%PDF-1.4 not a real pdf');
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'file' => $file,
]);
$response->assertStatus(422)->assertJsonValidationErrors('file');
});
it('rejects a file larger than the 5 MB limit', function () {
$file = UploadedFile::fake()->create('phones.csv', 5121); // KB, чуть больше лимита
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
'file' => $file,
]);
$response->assertStatus(422)->assertJsonValidationErrors('file');
});
it('returns 404 when uploading phones for another tenants campaign', function () {
$tenantB = Tenant::factory()->create();
$campaignB = AdCampaign::create([
'tenant_id' => $tenantB->id,
'name' => 'Чужая кампания',
'audience_days' => 10,
'weekly_budget_rub' => '1000.00',
]);
$response = $this->postJson("/api/advertising/campaigns/{$campaignB->id}/phones", [
'text' => '79000000008',
]);
$response->assertStatus(404);
$this->assertDatabaseMissing('ad_campaign_phones', ['campaign_id' => $campaignB->id]);
});