3fdcd88ad3
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса» не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой: в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета. Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов. Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт. Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице «Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов, их клиент разбирает сам. Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены. 1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам, 500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17: право на запись плюс нумератор. В плане про это было написано прямым текстом, я прошёл мимо. Тесты поймать не могли: ходят суперпользователем. 2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» — это норма, так и есть живьём. 3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит теперь сам джоб: показываем один раз, пока сказать нечего. 4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите. В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её больше не берёт. Сценарий существует только при частичном отказе — тест переписан на два объявления и теперь вырезание защиты его роняет. Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74, мест снятия заморозки денег по-прежнему четыре. На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
11 KiB
PHP
254 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\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
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);
|
|
});
|