Files
portal/app/tests/Unit/YandexAudienceClientTest.php
T
Дмитрий f9dea24ac4 fix(sales): сегмент в Яндексе подтверждается, а не остаётся черновиком
Создание сегмента — два шага, а не один: upload_csv_file даёт статус uploaded
(черновик, в списке Аудиторий его нет и Директу он не виден), и только
segment/{id}/confirm сохраняет сегмент. Второй шаг отсутствовал — 19.07 заливка
на бою отчиталась успехом, id записался, а в Аудиториях было пусто.
content_type для телефонов строго 'crm'. Регресс-тест закрывает.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:31:25 +03:00

135 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Sales\YandexAudienceClient;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
// tests/Unit (кроме Unit/Autopodbor) не привязан к TestCase в Pest.php —
// фасад Http без контейнера Laravel не поднимется.
uses(TestCase::class);
it('создаёт сегмент из CSV с заголовком phone', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600, 'status' => 'uploaded']], 200),
]);
$id = (new YandexAudienceClient('test-token'))->createSegment('Кандидаты', ['79990000001', '79990000002']);
expect($id)->toBe(58029600);
Http::assertSent(function (Request $r) {
if (! str_contains($r->url(), 'upload_csv_file')) {
return false;
}
expect($r->hasHeader('Authorization', 'OAuth test-token'))->toBeTrue();
expect($r->isMultipart())->toBeTrue();
// Тело multipart: имя файла и сам CSV c заголовком phone.
// Раскладываем в плоские массивы явно — иначе статанализ видит здесь mixed.
/** @var array<string, string> $filenames */
$filenames = [];
/** @var array<string, string> $contents */
$contents = [];
foreach ($r->data() as $part) {
if (! is_array($part) || ! isset($part['name']) || ! is_string($part['name'])) {
continue;
}
$filenames[$part['name']] = is_string($part['filename'] ?? null) ? $part['filename'] : '';
$contents[$part['name']] = is_string($part['contents'] ?? null) ? $part['contents'] : '';
}
expect($filenames)->toHaveKey('file');
expect($filenames['file'])->toBe('phones.csv');
expect($contents['file'])->toBe("phone\n79990000001\n79990000002\n");
return true;
});
});
it('после загрузки файла ОБЯЗАТЕЛЬНО подтверждает сегмент', function () {
// 🔴 Регресс на боевую поломку 19.07: без второго вызова confirm сегмент
// остаётся черновиком (status uploaded), в списке Аудиторий его нет,
// Директу он не виден — а база при этом пишет «успешно создан».
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600, 'status' => 'uploaded']], 200),
]);
(new YandexAudienceClient('test-token'))->createSegment('Кандидаты', ['79990000001', '79990000002']);
Http::assertSent(function (Request $r) {
if (! str_contains($r->url(), '/segment/58029600/confirm')) {
return false;
}
expect($r->hasHeader('Authorization', 'OAuth test-token'))->toBeTrue();
/** @var array<string, mixed> $body */
$body = $r->data();
/** @var array<string, mixed> $segment */
$segment = is_array($body['segment'] ?? null) ? $body['segment'] : [];
expect($segment['id'] ?? null)->toBe(58029600);
expect($segment['name'] ?? null)->toBe('Кандидаты');
expect($segment['hashed'] ?? null)->toBeFalse();
// Единственное верное значение для телефонов — 'crm'.
expect($segment['content_type'] ?? null)->toBe('crm');
return true;
});
});
it('не считает повторную заливку того же файла аварией', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['errors' => [['error_type' => 'no_changes']]], 400),
]);
$result = (new YandexAudienceClient('test-token'))->modifyData(58029600, ['79990000001'], 'addition');
expect($result)->toBe('no_changes');
Http::assertSent(function (Request $r) {
expect($r->url())->toContain('/v1/management/segment/58029600/modify_data');
expect($r->url())->toContain('modification_type=addition');
return true;
});
});
it('бросает исключение на настоящей ошибке заливки', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['errors' => [['error_type' => 'invalid_file']]], 400),
]);
(new YandexAudienceClient('test-token'))->modifyData(58029600, ['79990000001'], 'addition');
})->throws(RuntimeException::class);
it('читает охват и статус сегмента по id', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segments' => [
['id' => 111, 'name' => 'Чужой', 'status' => 'processed'],
['id' => 58029600, 'name' => 'Кандидаты', 'status' => 'processed', 'matched_quantity' => 4200],
]], 200),
]);
$segment = (new YandexAudienceClient('test-token'))->segment(58029600);
expect($segment['status'])->toBe('processed');
expect($segment['matched_quantity'])->toBe(4200);
Http::assertSent(fn (Request $r) => str_contains($r->url(), '/v1/management/segments')
&& $r->hasHeader('Authorization', 'OAuth test-token'));
});
it('отдаёт пустой массив, если сегмента нет в списке', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segments' => []], 200),
]);
expect((new YandexAudienceClient('test-token'))->segment(58029600))->toBe([]);
});