2026-07-24 22:26:30 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Services\Advertising\YandexDirectClient;
|
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
|
|
uses(TestCase::class);
|
|
|
|
|
|
|
|
|
|
|
|
it('adds a retargeting list referencing an audience segment', function () {
|
|
|
|
|
|
Http::fake([
|
|
|
|
|
|
'*/json/v5/retargetinglists' => Http::response(['result' => ['AddResults' => [['Id' => 777]]]], 200),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN');
|
|
|
|
|
|
$id = $client->addRetargetingList('Лидерра кампания #5', 58034825);
|
|
|
|
|
|
|
|
|
|
|
|
expect($id)->toBe(777);
|
|
|
|
|
|
Http::assertSent(function ($req) {
|
|
|
|
|
|
$body = $req->data();
|
|
|
|
|
|
|
|
|
|
|
|
return str_ends_with($req->url(), '/json/v5/retargetinglists')
|
|
|
|
|
|
&& $req->hasHeader('Authorization', 'Bearer TESTTOKEN')
|
|
|
|
|
|
&& $body['method'] === 'add'
|
|
|
|
|
|
&& $body['params']['RetargetingLists'][0]['Type'] === 'AUDIENCE'
|
|
|
|
|
|
&& $body['params']['RetargetingLists'][0]['Rules'][0]['Operator'] === 'ALL'
|
2026-07-30 10:57:38 +03:00
|
|
|
|
// Сегмент Аудиторий указывается с приставкой «20» — см. addRetargetingList().
|
|
|
|
|
|
&& $body['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]['ExternalId'] === 2058034825;
|
2026-07-24 22:26:30 +03:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('surfaces an API error as RuntimeException', function () {
|
|
|
|
|
|
Http::fake(['*/json/v5/campaigns' => Http::response(['error' => ['error_string' => 'Нет доступа к API', 'error_code' => 53]], 200)]);
|
|
|
|
|
|
$client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'T');
|
2026-07-27 08:48:08 +03:00
|
|
|
|
expect(fn () => $client->addCpmBannerCampaign('C', '2026-08-01', '2026-08-14', 72000000, 90000000, 3, 14))->toThrow(RuntimeException::class);
|
2026-07-24 22:26:30 +03:00
|
|
|
|
});
|
2026-08-01 15:12:40 +03:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔴 Язык ответа спрашиваем явно. Без заголовка Яндекс отвечает ПО-АНГЛИЙСКИ, и эти слова
|
|
|
|
|
|
* уезжают клиенту в переписку и письмом: «Rejected at moderation.», «Accepted at moderation.».
|
|
|
|
|
|
*
|
|
|
|
|
|
* Замер 01.08.2026, боевой ключ, два одинаковых запроса подряд по объявлению 17787641507:
|
|
|
|
|
|
*
|
|
|
|
|
|
* без заголовка → StatusClarification = "\nRejected at moderation."
|
|
|
|
|
|
* Accept-Language: ru → StatusClarification = "\nОтклонено на модерации."
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🪤 В `cabinet-flow.md` §7.7 записан русский ответ — и запись не врёт: тот замер делали
|
|
|
|
|
|
* ДРУГИМ инструментом, который язык просил. Разбор отписок построили по показаниям прибора,
|
|
|
|
|
|
* которым боевой код не пользуется. Мерить надо той же дорогой, по которой ходит продукт.
|
|
|
|
|
|
*/
|
|
|
|
|
|
it('asks Yandex to answer in Russian', function () {
|
|
|
|
|
|
Http::fake(['*/json/v5/ads' => Http::response(['result' => ['Ads' => []]], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
(new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'T'))->getAdsModeration([1]);
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(fn ($req) => $req->hasHeader('Accept-Language', 'ru'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔴 Отказ включения приходит в «предупреждениях», а не в «ошибках» — и молча теряется.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Замер 01.08.2026, боевой ключ, объявление 17787641513, ответ целиком:
|
|
|
|
|
|
*
|
|
|
|
|
|
* {"result":{"ResumeResults":[{"Id":17787641513,
|
|
|
|
|
|
* "Warnings":[{"Code":10201,"Message":"Объявление не остановлено"}],"Errors":[]}]}}
|
|
|
|
|
|
*
|
|
|
|
|
|
* `Errors` пуст, значит проверка по ошибкам молчит и портал считает, что справился.
|
|
|
|
|
|
* На бою это шло двое суток: обход каждые два часа, около двадцати попыток включить
|
|
|
|
|
|
* 13 объявлений, ноль записей в журнале, показов ноль. Клиент при этом видит «Идут показы».
|
|
|
|
|
|
*/
|
|
|
|
|
|
it('reports ads Yandex refused to resume instead of failing silently', function () {
|
|
|
|
|
|
Http::fake(['*/json/v5/ads' => Http::response(['result' => ['ResumeResults' => [
|
|
|
|
|
|
['Id' => 111, 'Warnings' => [['Code' => 10201, 'Message' => 'Объявление не остановлено']], 'Errors' => []],
|
|
|
|
|
|
['Id' => 222, 'Warnings' => [], 'Errors' => []],
|
|
|
|
|
|
]]], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$otkazy = (new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'T'))->resumeAds([111, 222]);
|
|
|
|
|
|
|
|
|
|
|
|
expect($otkazy)->toBe([111 => 'Объявление не остановлено']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** Всех включили — отказов нет, и это не путается с «мы не спросили». */
|
|
|
|
|
|
it('returns no refusals when every ad resumed', function () {
|
|
|
|
|
|
Http::fake(['*/json/v5/ads' => Http::response(['result' => ['ResumeResults' => [
|
|
|
|
|
|
['Id' => 111, 'Warnings' => [], 'Errors' => []],
|
|
|
|
|
|
]]], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
expect((new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'T'))->resumeAds([111]))->toBe([]);
|
|
|
|
|
|
});
|