Http::response(['result' => ['AddResults' => [['Id' => 777]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $id = $client->addCpmBannerCampaign( 'Лидерра медийная #1', '2026-07-26', '2026-08-26', 50_000_000, 1_000_000_000, 3, 7, ); expect($id)->toBe(777); Http::assertSent(function ($req) { $body = $req->data(); $campaign = $body['params']['Campaigns'][0]; $strategy = $campaign['CpmBannerCampaign']['BiddingStrategy']; $cpMax = $strategy['Network']['CpMaximumImpressions']; $freq = $campaign['CpmBannerCampaign']['FrequencyCap']; return $body['method'] === 'add' && $campaign['Name'] === 'Лидерра медийная #1' && $campaign['StartDate'] === '2026-07-26' && $campaign['EndDate'] === '2026-08-26' && $strategy['Search']['BiddingStrategyType'] === 'SERVING_OFF' && $strategy['Network']['BiddingStrategyType'] === 'CP_MAXIMUM_IMPRESSIONS' && $cpMax['AverageCpm'] === 50_000_000 && $cpMax['SpendLimit'] === 1_000_000_000 && $cpMax['StartDate'] === '2026-07-26' && $cpMax['EndDate'] === '2026-08-26' && $cpMax['AutoContinue'] === 'NO' && $freq['Impressions'] === 3 && $freq['PeriodDays'] === 7; }); }); it('sends auto continue yes when requested for the cpm banner campaign', function () { Http::fake([ '*/json/v5/campaigns' => Http::response(['result' => ['AddResults' => [['Id' => 778]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $client->addCpmBannerCampaign( 'Лидерра медийная #2', '2026-07-26', '2026-08-26', 50_000_000, 1_000_000_000, 3, 7, 'YES', ); Http::assertSent(function ($req) { $body = $req->data(); return $body['params']['Campaigns'][0]['CpmBannerCampaign']['BiddingStrategy']['Network']['CpMaximumImpressions']['AutoContinue'] === 'YES'; }); }); it('adds an empty cpm banner ad group with an empty keywords object', function () { Http::fake([ '*/json/v5/adgroups' => Http::response(['result' => ['AddResults' => [['Id' => 555]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $id = $client->addCpmBannerAdGroup(777, 'Группа медийная', [1, 213]); expect($id)->toBe(555); Http::assertSent(function ($req) { $body = $req->data(); $group = $body['params']['AdGroups'][0]; $matches = $body['method'] === 'add' && $group['Name'] === 'Группа медийная' && $group['CampaignId'] === 777 && $group['RegionIds'] === [1, 213]; // Пустой JSON-объект {}, а не массив [] $decoded = json_decode($req->body()); $matches = $matches && $decoded->params->AdGroups[0]->CpmBannerKeywordsAdGroup instanceof stdClass; return $matches && str_contains($req->body(), '"CpmBannerKeywordsAdGroup":{}'); }); }); it('adds a media audience target without a context bid', function () { Http::fake([ '*/json/v5/audiencetargets' => Http::response(['result' => ['AddResults' => [['Id' => 999]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $id = $client->addMediaAudienceTarget(555, 58034825); expect($id)->toBe(999); Http::assertSent(function ($req) { $body = $req->data(); $target = $body['params']['AudienceTargets'][0]; return $body['method'] === 'add' && $target['AdGroupId'] === 555 && $target['RetargetingListId'] === 58034825 && ! array_key_exists('ContextBid', $target); }); }); it('adds a cpm banner ad by creative id', function () { Http::fake([ '*/json/v5/ads' => Http::response(['result' => ['AddResults' => [['Id' => 111]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $id = $client->addCpmBannerAd(555, 4242, 'https://liderra.ru/promo'); expect($id)->toBe(111); Http::assertSent(function ($req) { $body = $req->data(); $ad = $body['params']['Ads'][0]; return $body['method'] === 'add' && $ad['AdGroupId'] === 555 && $ad['CpmBannerAdBuilderAd']['Creative']['CreativeId'] === 4242 && $ad['CpmBannerAdBuilderAd']['Href'] === 'https://liderra.ru/promo'; }); }); it('parses a creative preview into an array with is_adaptive true when YES', function () { Http::fake([ '*/json/v5/creatives' => Http::response(['result' => ['Creatives' => [[ 'Id' => 4242, 'Type' => 'HTML5_CREATIVE', 'PreviewUrl' => 'https://yastatic.net/preview/4242', 'ThumbnailUrl' => 'https://yastatic.net/thumb/4242', 'IsAdaptive' => 'YES', 'Width' => 300, 'Height' => 250, ]]]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $preview = $client->getCreativePreview(4242); expect($preview)->toBe([ 'id' => 4242, 'type' => 'HTML5_CREATIVE', 'preview_url' => 'https://yastatic.net/preview/4242', 'thumbnail_url' => 'https://yastatic.net/thumb/4242', 'is_adaptive' => true, 'width' => 300, 'height' => 250, ]); Http::assertSent(function ($req) { $body = $req->data(); return $body['method'] === 'get' && $body['params']['SelectionCriteria']['Ids'] === [4242] && $body['params']['FieldNames'] === ['Id', 'Type', 'PreviewUrl', 'ThumbnailUrl', 'IsAdaptive', 'Width', 'Height']; }); }); it('returns null from creative preview when the creative is not found', function () { Http::fake([ '*/json/v5/creatives' => Http::response(['result' => ['Creatives' => []]], 200), ]); $client = new YandexDirectClient('https://api-sandbox.direct.yandex.com', 'TESTTOKEN'); $preview = $client->getCreativePreview(9999); expect($preview)->toBeNull(); }); it('lists image creative ids with their sizes and ignores video creatives', function () { Http::fake([ '*/json/v5/creatives' => Http::response(['result' => ['Creatives' => [ ['Id' => 1163441165, 'Type' => 'VIDEO_EXTENSION_CREATIVE', 'Width' => 1080, 'Height' => 1080], ['Id' => 1163586673, 'Type' => 'HTML5_CREATIVE', 'Width' => 300, 'Height' => 250], ['Id' => 1163586680, 'Type' => 'HTML5_CREATIVE', 'Width' => 728, 'Height' => 90], ]]]), ]); $client = new YandexDirectClient('https://api.direct.yandex.com', 'T'); // Картиночные креативы Яндекс хранит типом HTML5_CREATIVE даже для загруженных JPG — // проверено живым запросом 27.07.2026. Видео-креативы в набор попадать не должны. expect($client->listImageCreativeIds())->toBe([ 1163586673 => [300, 250], 1163586680 => [728, 90], ]); }); /** * Слепок креативов — это опись всего аккаунта, и Яндекс отдаёт её страницами: больше * десяти тысяч объектов за раз он не отдаст никогда, а признак «отдал не всё» — поле * `LimitedBy` в ответе. Раньше страницы не листались вовсе: слепок молча обрывался на * первой. Дальше опознание креативов сравнивает «до» и «после» — при оборванном слепке * креатив, лежащий за границей страницы, выглядел бы «новым» на каждом заходе. */ it('walks through all pages of the creatives snapshot', function () { $pages = [ // Первая страница: LimitedBy говорит «отдал не всё, продолжай с этого места». ['result' => [ 'Creatives' => [['Id' => 11, 'Type' => 'HTML5_CREATIVE', 'Width' => 300, 'Height' => 250]], 'LimitedBy' => 1, ]], // Вторая: LimitedBy больше нет — это конец описи. ['result' => [ 'Creatives' => [['Id' => 22, 'Type' => 'HTML5_CREATIVE', 'Width' => 728, 'Height' => 90]], ]], ]; $offsets = []; Http::fake(function ($request) use (&$pages, &$offsets) { $offsets[] = $request['params']['Page']['Offset'] ?? null; return Http::response(array_shift($pages) ?? ['result' => ['Creatives' => []]]); }); $client = new YandexDirectClient('https://api.direct.yandex.com', 'T'); expect($client->listImageCreativeIds())->toBe([ 11 => [300, 250], 22 => [728, 90], ]); // Ровно два обращения: второе — с продолжением с того места, где Яндекс оборвал первое. expect($offsets)->toBe([0, 1]); }); it('returns an empty snapshot when the account has no creatives', function () { Http::fake(['*/json/v5/creatives' => Http::response(['result' => []])]); $client = new YandexDirectClient('https://api.direct.yandex.com', 'T'); expect($client->listImageCreativeIds())->toBe([]); });