create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false, ]); return [$tenant, $campaign]; } it('текст сообщения хранится целиком, длиннее 255 знаков в том числе', function () { [$tenant, $campaign] = messageCampaign(); // Модератор перечисляет претензии списком — 255 знаков не хватает. Именно предел // колонки moderation_reason уже валил обход модерации целиком. $long = str_repeat('Причина отказа очень подробная. ', 40); $msg = AdCampaignMessage::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'author' => AdCampaignMessage::AUTHOR_YANDEX, 'body' => $long, ]); expect(mb_strlen($msg->refresh()->body))->toBe(mb_strlen($long)); }); it('пояснение Яндекса ложится в ленту', function () { [$tenant, $campaign] = messageCampaign(); app(CampaignMessageService::class)->postFromYandex($campaign, null, 'Изображение не соответствует требованиям'); $messages = AdCampaignMessage::where('campaign_id', $campaign->id)->get(); expect($messages)->toHaveCount(1) ->and($messages->first()->author)->toBe(AdCampaignMessage::AUTHOR_YANDEX) ->and($messages->first()->body)->toBe('Изображение не соответствует требованиям'); }); /** * Опрос модерации бежит по расписанию. Пока Яндекс не передумал, он присылает ОДНУ И ТУ ЖЕ * причину при каждом обходе. Без защиты лента за сутки превратится в сотню одинаковых строк, * и клиенту прилетит сотня писем. */ it('тот же текст по тому же баннеру второй раз в ленту не попадает', function () { [$tenant, $campaign] = messageCampaign(); $banner = AdCampaignBanner::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true, 'path' => 'ad-banners/a/300x250.jpg', ]); $service = app(CampaignMessageService::class); $service->postFromYandex($campaign, $banner->id, 'Одно и то же'); $second = $service->postFromYandex($campaign, $banner->id, 'Одно и то же'); expect($second)->toBeNull() ->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(1); }); /** Другой размер блока — другая беда, её надо показать отдельно. */ it('тот же текст по ДРУГОМУ баннеру в ленту попадает', function () { [$tenant, $campaign] = messageCampaign(); $slot = [ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'bytes' => 1000, 'included' => true, ]; $a = AdCampaignBanner::create($slot + ['width' => 300, 'height' => 250, 'path' => 'ad-banners/a/300x250.jpg']); $b = AdCampaignBanner::create($slot + ['width' => 728, 'height' => 90, 'path' => 'ad-banners/a/728x90.jpg']); $service = app(CampaignMessageService::class); $service->postFromYandex($campaign, $a->id, 'Одно и то же'); $service->postFromYandex($campaign, $b->id, 'Одно и то же'); expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(2); }); /** Яндекс передумал и написал другое — это новость, её показываем. */ it('изменившийся текст по тому же баннеру в ленту попадает', function () { [$tenant, $campaign] = messageCampaign(); $banner = AdCampaignBanner::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true, 'path' => 'ad-banners/a/300x250.jpg', ]); $service = app(CampaignMessageService::class); $service->postFromYandex($campaign, $banner->id, 'Первая претензия'); $service->postFromYandex($campaign, $banner->id, 'Вторая претензия'); expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(2); }); it('пустой текст в ленту не кладём', function () { [, $campaign] = messageCampaign(); expect(app(CampaignMessageService::class)->postFromYandex($campaign, null, ' '))->toBeNull() ->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0); }); /** * 🪤 Ловушка, которую открыла разведка, и которую нельзя чинить дедупом по всей истории. * * Клиент починил рекламу, отправил заново — и получил ТОТ ЖЕ отказ. Это новость: клиент * должен узнать, что его опять не пустили, и по той же причине. Дедуп «такой текст уже * когда-то был» съел бы это сообщение, и клиент остался бы без объяснения. * * Поэтому защита от дублей смотрит только на ПОСЛЕДНЕЕ сообщение, а «заглушку» держит * не она, а джоб модерации — см. SyncCampaignModerationJobTest. */ it('тот же отказ после доклада разведки клиент увидит снова', function () { [$tenant, $campaign] = messageCampaign(); $banner = AdCampaignBanner::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true, 'path' => 'ad-banners/a/300x250.jpg', ]); $service = app(CampaignMessageService::class); $service->postFromRobot($campaign, $banner->id, 'Нет предупреждения: финансовые услуги'); $service->postFromYandex($campaign, $banner->id, 'Отклонено'); $again = $service->postFromRobot($campaign, $banner->id, 'Нет предупреждения: финансовые услуги'); expect($again)->not->toBeNull() ->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(3); }); /** Доклад разведки умеет нести снимок экрана — он и есть доказательство прочитанного. */ it('доклад разведки кладётся со снимком экрана', function () { [$tenant, $campaign] = messageCampaign(); $message = app(CampaignMessageService::class)->postFromRobot($campaign, null, 'Причина', [ 'path' => 'ad-messages/1/1/okno.png', 'name' => 'okno.png', 'size' => 1024, 'mime' => 'image/png', ]); expect($message->file_name)->toBe('okno.png') ->and($message->file_path)->toBe('ad-messages/1/1/okno.png') ->and($message->author)->toBe(AdCampaignMessage::AUTHOR_YANDEX); }); /** Пустой доклад — не доклад. В ленту не кладём. */ it('пустой доклад разведки в ленту не попадает', function () { [, $campaign] = messageCampaign(); expect(app(CampaignMessageService::class)->postFromRobot($campaign, null, ' '))->toBeNull() ->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0); }); /** Признак «Яндекс уже что-то сказал» — на нём держится защита от повторной заглушки. */ it('признак «слова Яндекса уже есть» считается по баннеру, а не по кампании целиком', function () { [$tenant, $campaign] = messageCampaign(); $slot = ['tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'bytes' => 1000, 'included' => true]; $a = AdCampaignBanner::create($slot + ['width' => 300, 'height' => 250, 'path' => 'ad-banners/a/300x250.jpg']); $b = AdCampaignBanner::create($slot + ['width' => 728, 'height' => 90, 'path' => 'ad-banners/a/728x90.jpg']); $service = app(CampaignMessageService::class); $service->postFromYandex($campaign, $a->id, 'Отклонено'); expect($service->hasFromYandex($campaign, $a->id))->toBeTrue() ->and($service->hasFromYandex($campaign, $b->id))->toBeFalse(); });