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' // Сегмент Аудиторий указывается с приставкой «20» — см. addRetargetingList(). && $body['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]['ExternalId'] === 2058034825; }); }); 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'); expect(fn () => $client->addCpmBannerCampaign('C', '2026-08-01', '2026-08-14', 72000000, 90000000, 3, 14))->toThrow(RuntimeException::class); }); /** * 🔴 Язык ответа спрашиваем явно. Без заголовка Яндекс отвечает ПО-АНГЛИЙСКИ, и эти слова * уезжают клиенту в переписку и письмом: «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([]); });