Files
portal/app/tests/Feature/Advertising/CampaignMessageEndpointsTest.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

259 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Mail\AdDocumentAttachedMail;
use App\Models\AdCampaign;
use App\Models\AdCampaignMessage;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
// Откат после теста. Без него записи файла остаются в базе и роняют ЧУЖИЕ тесты:
// 01.08.2026 девятнадцать чужих проводок уронили денежную проверку приёмника МТС.
uses(DatabaseTransactions::class);
function messageEndpointCampaign(): array
{
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$campaign = AdCampaign::create([
'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false,
]);
return [$tenant, $user, $campaign];
}
it('клиент видит ленту своей кампании по порядку', function () {
[$tenant, $user, $campaign] = messageEndpointCampaign();
AdCampaignMessage::create([
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
'author' => AdCampaignMessage::AUTHOR_YANDEX, 'body' => 'Первое',
]);
AdCampaignMessage::create([
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
'author' => AdCampaignMessage::AUTHOR_CLIENT, 'body' => 'Второе',
]);
$res = $this->actingAs($user)->getJson("/api/advertising/campaigns/{$campaign->id}/messages");
$res->assertOk();
expect($res->json('messages.0.body'))->toBe('Первое')
->and($res->json('messages.0.author'))->toBe(AdCampaignMessage::AUTHOR_YANDEX)
->and($res->json('messages.1.body'))->toBe('Второе');
});
/**
* Наценка — наша кухня, клиенту её видеть нельзя нигде, включая переписку.
* Ручка отдаёт строго перечисленные поля сообщения, и так должно остаться.
*/
it('в ленте нет ни наценки, ни закупочной цены', function () {
[$tenant, $user, $campaign] = messageEndpointCampaign();
AdCampaignMessage::create([
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
'author' => AdCampaignMessage::AUTHOR_YANDEX, 'body' => 'Изображение не подошло',
]);
$res = $this->actingAs($user)->getJson("/api/advertising/campaigns/{$campaign->id}/messages");
$res->assertOk();
expect($res->getContent())->not->toContain('yandex_cost_rub')
->and($res->getContent())->not->toContain('ad_margin_percent')
->and(array_keys($res->json('messages.0')))->toBe([
'id', 'author', 'banner_id', 'body', 'file_name', 'file_size', 'created_at',
]);
});
/** Чужая переписка — чужие бумаги и чужие претензии модератора. */
it('чужую ленту клиент не видит', function () {
[$tenantA, , $campaignA] = messageEndpointCampaign();
AdCampaignMessage::create([
'tenant_id' => $tenantA->id, 'campaign_id' => $campaignA->id,
'author' => AdCampaignMessage::AUTHOR_YANDEX, 'body' => 'Чужое',
]);
[, $userB] = messageEndpointCampaign();
$res = $this->actingAs($userB)->getJson("/api/advertising/campaigns/{$campaignA->id}/messages");
$res->assertStatus(404);
expect($res->getContent())->not->toContain('Чужое');
});
it('без входа лента не отдаётся', function () {
[, , $campaign] = messageEndpointCampaign();
$this->getJson("/api/advertising/campaigns/{$campaign->id}/messages")->assertStatus(401);
});
it('клиент отправляет ответ с документом', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'body' => 'Прикладываю свидетельство',
'file' => UploadedFile::fake()->create('svidetelstvo.pdf', 200, 'application/pdf'),
]);
$res->assertStatus(201);
$message = AdCampaignMessage::where('campaign_id', $campaign->id)->first();
expect($message->author)->toBe(AdCampaignMessage::AUTHOR_CLIENT)
->and($message->body)->toBe('Прикладываю свидетельство')
->and($message->file_name)->toBe('svidetelstvo.pdf')
->and($message->file_path)->not->toBeNull();
Storage::disk('local')->assertExists($message->file_path);
});
it('ответ без текста и без файла не принимается', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
$this->actingAs($user)
->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [])
->assertStatus(422);
expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0);
});
/**
* Приложение исполняемого файла — не «неудобство», а дыра: файл ляжет на диск боевого
* сервера, и дальше вопрос только в том, кто его оттуда позовёт.
*/
it('файл не того типа отклоняется и на диск не ложится', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'body' => 'вот',
'file' => UploadedFile::fake()->create('opasno.exe', 10, 'application/octet-stream'),
]);
$res->assertStatus(422);
$res->assertJsonValidationErrors('file');
expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0)
->and(Storage::disk('local')->allFiles())->toBe([]);
});
it('файл тяжелее предела отклоняется', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'file' => UploadedFile::fake()->create('big.pdf', 10241, 'application/pdf'),
]);
$res->assertStatus(422);
expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0)
->and(Storage::disk('local')->allFiles())->toBe([]);
});
it('в чужую кампанию написать нельзя', function () {
Storage::fake('local');
[, , $campaignA] = messageEndpointCampaign();
[, $userB] = messageEndpointCampaign();
$this->actingAs($userB)
->postJson("/api/advertising/campaigns/{$campaignA->id}/messages", ['body' => 'привет'])
->assertStatus(404);
expect(AdCampaignMessage::where('campaign_id', $campaignA->id)->count())->toBe(0);
});
it('свой файл клиент скачивает, чужой — нет', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'file' => UploadedFile::fake()->create('doc.pdf', 100, 'application/pdf'),
])->assertStatus(201);
$message = AdCampaignMessage::where('campaign_id', $campaign->id)->first();
$this->actingAs($user)
->get("/api/advertising/campaigns/{$campaign->id}/messages/{$message->id}/file")
->assertOk();
[, $userB] = messageEndpointCampaign();
$this->actingAs($userB)
->get("/api/advertising/campaigns/{$campaign->id}/messages/{$message->id}/file")
->assertStatus(404);
});
/**
* 🔴 Задача 16 закрыта по факту: везти документ роботом НЕКУДА. Проверено двумя нарочными
* отказами — в окне отказа Яндекса ноль полей для файла, документы он принимает только
* снаружи кабинета. Значит портал обязан сказать клиенту правду: файл у нас, сам он
* никуда не уедет. Молчать нельзя — клиент будет ждать ответа, которого не будет.
*/
it('приложил документ — портал честно говорит, что сам он к Яндексу не уедет', function () {
Storage::fake('local');
Mail::fake();
[, $user, $campaign] = messageEndpointCampaign();
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'body' => 'Прикладываю лицензию',
'file' => UploadedFile::fake()->create('licence.pdf', 200, 'application/pdf'),
])->assertStatus(201);
$note = AdCampaignMessage::where('campaign_id', $campaign->id)
->where('author', AdCampaignMessage::AUTHOR_SYSTEM)
->latest('id')->first();
expect($note)->not->toBeNull()
->and($note->body)->toContain('Яндекс');
});
/** И зовём владельца письмом: без этого «разберёмся вручную» было бы пустым обещанием. */
it('о приложенном документе владелец узнаёт письмом', function () {
Storage::fake('local');
Mail::fake();
[, $user, $campaign] = messageEndpointCampaign();
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'file' => UploadedFile::fake()->create('licence.pdf', 200, 'application/pdf'),
])->assertStatus(201);
Mail::assertQueued(AdDocumentAttachedMail::class);
});
/** Обычный ответ без файла — обычный ответ. Ни отметки, ни письма владельцу. */
it('ответ без документа отметки не порождает и владельца не дёргает', function () {
Storage::fake('local');
Mail::fake();
[, $user, $campaign] = messageEndpointCampaign();
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'body' => 'Переделал картинку',
])->assertStatus(201);
expect(AdCampaignMessage::where('campaign_id', $campaign->id)
->where('author', AdCampaignMessage::AUTHOR_SYSTEM)->count())->toBe(0);
Mail::assertNothingQueued();
});
/**
* Отметка и письмо — дело второстепенное, а сообщение клиента с документом нет. Беда
* с почтой не должна возвращать клиенту отказ на успешно принятый файл.
*/
it('упавшая почта не мешает принять документ клиента', function () {
Storage::fake('local');
[, $user, $campaign] = messageEndpointCampaign();
Mail::shouldReceive('to')->andThrow(new RuntimeException('почта легла'));
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/messages", [
'file' => UploadedFile::fake()->create('licence.pdf', 200, 'application/pdf'),
])->assertStatus(201);
expect(AdCampaignMessage::where('campaign_id', $campaign->id)
->where('author', AdCampaignMessage::AUTHOR_CLIENT)->count())->toBe(1);
});