Files
portal/app/tests/Feature/ClientTg/AutoRuleZagolovokMediaTest.php
T
Дмитрий 7922bffb6d
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat(телеграм-реклама): заголовок и картинка в авто-режиме + честная проверка медиа по требованиям МТС
Пачка 1 замечаний владельца о едином виде рекламных экранов. Песочница выключена
02.08.2026 — каждая дыра ниже стоила живых денег.

1. Заголовок объявления в АВТО-правиле. Кабинет МТС требует его для рекламы сайта;
   утром 02.08 поле довезли до разовой формы, а в авто его не было вовсе — накопитель
   создавал кампанию без ad_headline, и она сгорела бы в кабинете уже после списания.
   Колонка client_tg_auto_rule.ad_headline, приём в контроллере (обязателен только при
   включённом авто и не-телеграмной ссылке), перенос в кампанию, поле на экране.

2. Картинка/видео в авто-правиле. Колонка media_path, отдельная загрузка
   POST /api/telegram/auto-rule/media, перенос в кампанию, поле на экране.

3. Проверка медиа переписана по настоящим требованиям кабинета, снятым глазами 02.08.
   Было mimes:png,jpg,jpeg,gif,mp4|max:51200 — врало по пяти пунктам: принимало GIF,
   пропускало вдвое больший вес, не смотрело пиксели и длительность, зря отказывало
   в mov/webm. Стало правило App\Rules\ClientTg\MtsMedia: картинка JPEG/PNG до 25 МБ
   и 640x360...5120x2880, видео до 20 МБ, 3-55 секунд, от 640x360. Формат картинки
   определяется по содержимому, длительность и кадр видео читает App\Support\Mp4Probe
   из контейнера — без внешних программ. Отказ человеческий: «Картинка слишком
   маленькая: 300x200. Нужна не меньше 640x360».

4. Цена показа с медиа — 600 руб. с картинкой, 680 с видео — теперь видна ДО загрузки
   файла, на обоих экранах.

Сверх плана, найдено по дороге:

5. Предохранитель накопителя: правило, сохранённое до 02.08 со ссылкой на сайт и пустым
   заголовком, всё равно ушло бы в кабинет. Теперь такая пачка держится черновиком,
   в журнал пишется причина no_headline.

6. Отказ сервера доходит до клиента его словами. Оба экрана глушили ответ общей фразой
   «Не удалось рассчитать кампанию», и человек не понимал, что не так с файлом.

Границы честности: длительность и размер кадра читаются только у mp4/mov/m4v; у
webm/mkv/mpeg/wmv проверяются формат и вес — так и записано в Mp4Probe.

Проверено: 281 тест телеграм-модуля, 1753 фронтенд-теста, статанализ 0, Pint чист.
Глазами НЕ принимали — приёмка живьём в пачке 5. На боевой не выкачено.

Журнал схемы: v9.64 — номер взят как максимум по всем веткам плюс один, чтобы не
повторить столкновения 29.07 и 01.08.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 13:41:26 +03:00

233 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\ClientTg\AutoRule;
use App\Models\ClientTg\Campaign;
use App\Models\Tenant;
use App\Models\User;
use App\Services\ClientTg\TelegramAutoAccumulator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
/**
* Заголовок объявления и медиа в АВТО-правиле Telegram-рекламы (замечание З-5).
*
* 🔴 Дыра, которую закрываем: 02.08.2026 утром «Заголовок объявления» доставили на
* разовую форму — кабинет МТС требует его, когда реклама ведёт на САЙТ. В авто-режиме
* поля не было вовсе: `TelegramAutoAccumulator::openBatch` создавал кампанию без
* `ad_headline`, а `AutoRuleController` такого поля даже не принимал. Клиент, включивший
* авто со ссылкой на свой сайт, получил бы провал кампании в кабинете — и теперь на
* ЖИВЫХ деньгах (песочница выключена 02.08.2026 в 07:40 МСК).
*
* Картинки в авто тоже не было — по той же причине «не сделали». Запрета у МТС нет:
* объявление то же, кабинет тот же.
*
* Синтетические номера 7999… и файлы — реальных ПДн нет.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake();
Storage::fake('local');
$this->withHeaders(['Accept' => 'application/json']);
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
config(['client_tg.auto_batch_threshold' => 3]); // в тестах порог занижен
});
/** Тело сохранения правила: по умолчанию — реклама сайта (там заголовок обязателен). */
function pravilo(array $override = []): array
{
return array_merge([
'enabled' => true,
'ad_text' => 'Займы под залог за час',
'ad_link' => 'https://example.com/zaim',
'ad_headline' => 'Ломбард «Омега»',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '2500.00',
'daily_limit_rub' => '5000.00',
'batch_threshold' => 400,
], $override);
}
// ── Заголовок ───────────────────────────────────────────────────────────────
it('сохраняет заголовок объявления и отдаёт его обратно', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
expect(AutoRule::where('tenant_id', $this->tenant->id)->first()->ad_headline)
->toBe('Ломбард «Омега»');
$this->getJson('/api/telegram/auto-rule')
->assertOk()
->assertJsonPath('ad_headline', 'Ломбард «Омега»');
});
it('реклама САЙТА без заголовка не сохраняется → 422', function () {
$this->putJson('/api/telegram/auto-rule', pravilo(['ad_headline' => '']))
->assertStatus(422)
->assertJsonValidationErrors('ad_headline');
expect(AutoRule::where('tenant_id', $this->tenant->id)->count())->toBe(0);
});
it('реклама телеграм-канала без заголовка сохраняется — там его кабинет не спрашивает', function () {
$this->putJson('/api/telegram/auto-rule', pravilo([
'ad_link' => 'https://t.me/example_channel',
'ad_headline' => '',
]))->assertOk();
expect(AutoRule::where('tenant_id', $this->tenant->id)->first()->ad_headline)->toBeNull();
});
it('выключенное авто без заголовка сохраняется — правило ещё не работает', function () {
$this->putJson('/api/telegram/auto-rule', pravilo(['enabled' => false, 'ad_headline' => '']))
->assertOk();
});
it('заголовок длиннее 40 знаков не проходит — столько же, сколько в кабинете', function () {
$this->putJson('/api/telegram/auto-rule', pravilo(['ad_headline' => str_repeat('я', 41)]))
->assertStatus(422)
->assertJsonValidationErrors('ad_headline');
});
it('накопитель кладёт заголовок из правила в созданную авто-кампанию', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
app(TelegramAutoAccumulator::class)->accumulate($this->tenant->id, '79990000001');
$campaign = Campaign::where('tenant_id', $this->tenant->id)->firstOrFail();
expect($campaign->ad_headline)->toBe('Ломбард «Омега»');
});
// ── Картинка / видео ────────────────────────────────────────────────────────
it('прикладывает картинку к авто-правилу: файл сохранён, путь записан', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
$res = $this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('ad.jpg', 1280, 720),
]);
$res->assertOk();
$path = AutoRule::where('tenant_id', $this->tenant->id)->first()->media_path;
expect($path)->not->toBeNull();
Storage::disk('local')->assertExists($path);
expect($res->json('media_path'))->toBe($path);
});
it('картинка авто-правила проверяется теми же требованиями МТС', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
$res = $this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('ad.jpg', 300, 200),
]);
$res->assertStatus(422);
expect((string) $res->json('errors.media.0'))->toContain('640');
});
it('замена картинки авто-правила удаляет прежний файл', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
$this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('first.jpg', 1280, 720),
])->assertOk();
$first = AutoRule::where('tenant_id', $this->tenant->id)->first()->media_path;
$this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('second.jpg', 1280, 720),
])->assertOk();
$second = AutoRule::where('tenant_id', $this->tenant->id)->first()->media_path;
expect($second)->not->toBe($first);
Storage::disk('local')->assertMissing($first);
Storage::disk('local')->assertExists($second);
});
it('накопитель кладёт картинку из правила в созданную авто-кампанию', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
$this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('ad.jpg', 1280, 720),
])->assertOk();
$rulePath = AutoRule::where('tenant_id', $this->tenant->id)->first()->media_path;
app(TelegramAutoAccumulator::class)->accumulate($this->tenant->id, '79990000001');
expect(Campaign::where('tenant_id', $this->tenant->id)->firstOrFail()->media_path)
->toBe($rulePath);
});
it('сохранение правила НЕ стирает уже приложенную картинку', function () {
$this->putJson('/api/telegram/auto-rule', pravilo())->assertOk();
$this->post('/api/telegram/auto-rule/media', [
'media' => UploadedFile::fake()->image('ad.jpg', 1280, 720),
])->assertOk();
$this->putJson('/api/telegram/auto-rule', pravilo(['ad_text' => 'Другой текст']))->assertOk();
expect(AutoRule::where('tenant_id', $this->tenant->id)->first()->media_path)->not->toBeNull();
});
// ── Предохранитель накопителя ───────────────────────────────────────────────
it('старое правило: реклама САЙТА без заголовка — пачку не запускаем, деньги целы', function () {
// Такое правило нельзя сохранить через кабинет (валидация не пустит), но оно могло
// остаться от прежних версий — до 02.08.2026 поля заголовка не существовало вовсе.
// Запусти мы его, кабинет МТС встал бы на шаге «Объявление» уже ПОСЛЕ списания.
AutoRule::create([
'tenant_id' => $this->tenant->id,
'enabled' => true,
'ad_text' => 'Займы под залог',
'ad_link' => 'https://example.com/zaim',
'ad_headline' => null,
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '2500.00',
'daily_limit_rub' => '5000.00',
]);
foreach (['79990000001', '79990000002', '79990000003', '79990000004'] as $phone) {
app(TelegramAutoAccumulator::class)->accumulate($this->tenant->id, $phone);
}
$campaign = Campaign::where('tenant_id', $this->tenant->id)->firstOrFail();
expect($campaign->status)->toBe(Campaign::STATUS_DRAFT);
Queue::assertNothingPushed();
});
it('то же правило со ссылкой на канал — запускается: там заголовок не нужен', function () {
AutoRule::create([
'tenant_id' => $this->tenant->id,
'enabled' => true,
'ad_text' => 'Заходите в канал',
'ad_link' => 'https://t.me/example_channel',
'ad_headline' => null,
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '2500.00',
'daily_limit_rub' => '5000.00',
]);
foreach (['79990000001', '79990000002', '79990000003', '79990000004'] as $phone) {
app(TelegramAutoAccumulator::class)->accumulate($this->tenant->id, $phone);
}
expect(Campaign::where('tenant_id', $this->tenant->id)->firstOrFail()->status)
->toBe(Campaign::STATUS_QUEUED);
});
it('изоляция тенантов: каждый видит только своё правило', function () {
$this->putJson('/api/telegram/auto-rule', pravilo(['ad_headline' => 'Наш заголовок']))->assertOk();
$tenantB = Tenant::factory()->create();
$userB = User::factory()->create(['tenant_id' => $tenantB->id]);
$this->actingAs($userB)
->getJson('/api/telegram/auto-rule')
->assertOk()
->assertJsonPath('ad_headline', '');
});