2026-07-26 23:53:31 +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 cpm banner media campaign with cp maximum impressions strategy', function () {
|
|
|
|
|
Http::fake([
|
|
|
|
|
'*/json/v5/campaigns' => 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();
|
|
|
|
|
});
|
2026-07-27 14:33:02 +03:00
|
|
|
|
|
|
|
|
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],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-28 05:37:56 +03:00
|
|
|
/**
|
|
|
|
|
* Слепок креативов — это опись всего аккаунта, и Яндекс отдаёт её страницами: больше
|
|
|
|
|
* десяти тысяч объектов за раз он не отдаст никогда, а признак «отдал не всё» — поле
|
|
|
|
|
* `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]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-27 14:33:02 +03:00
|
|
|
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([]);
|
|
|
|
|
});
|