9ade3115d1
EXA за Cloudflare режет российские IP, федеральный канал возвращал пусто с боевого. Добавлен services.exa.proxy (env EXA_PROXY) → Guzzle proxy option в findSite/findSites. Пусто → напрямую (dev). Тест: с proxy цепочка не ломается, домен извлекается (8/8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
6.4 KiB
PHP
144 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\ChannelB\ExaSiteFinder;
|
|
use Illuminate\Http\Client\Factory as HttpFactory;
|
|
use Illuminate\Support\Facades\Config;
|
|
|
|
// Канал В, нормализация (ZAFIKSIROVANO / §11.5): у федерала НЕТ карточки в 2ГИС/Яндексе на регион,
|
|
// поэтому его САЙТ ищем через EXA по имени. Чёрный список каталогов/агрегаторов/иностранных TLD
|
|
// (§11.4) — не считать их сайтом фирмы. Сеть мокаем.
|
|
|
|
it('без ключа возвращает null', function () {
|
|
Config::set('services.exa.key', '');
|
|
|
|
$f = new ExaSiteFinder(app(HttpFactory::class));
|
|
|
|
expect($f->findSite('CarMoney', 'Красноярский край'))->toBeNull();
|
|
});
|
|
|
|
it('берёт домен первого результата (голый, без www/пути)', function () {
|
|
Config::set('services.exa', ['key' => 'k', 'base_url' => 'https://api.exa.ai', 'timeout_sec' => 30]);
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'api.exa.ai/*' => $http->response([
|
|
'results' => [['url' => 'https://www.carmoney.ru/about?utm=1'], ['url' => 'https://carmoney.ru']],
|
|
], 200),
|
|
]);
|
|
|
|
$f = new ExaSiteFinder($http);
|
|
|
|
expect($f->findSite('CarMoney', 'Красноярский край'))->toBe('carmoney.ru');
|
|
});
|
|
|
|
it('пропускает агрегаторы/каталоги и иностранные TLD, берёт первый «настоящий»', function () {
|
|
Config::set('services.exa.key', 'k');
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'api.exa.ai/*' => $http->response([
|
|
'results' => [
|
|
['url' => 'https://www.avito.ru/krasnoyarsk/zaim'],
|
|
['url' => 'https://banki.ru/products/cashmotor'],
|
|
['url' => 'https://cashmotor.kg/'], // иностранный TLD
|
|
['url' => 'https://cashmotor.ru/'], // настоящий
|
|
],
|
|
], 200),
|
|
]);
|
|
|
|
$f = new ExaSiteFinder($http);
|
|
|
|
expect($f->findSite('Cashmotor', 'Красноярский край'))->toBe('cashmotor.ru');
|
|
});
|
|
|
|
it('нет подходящих результатов или ошибка сети → null', function () {
|
|
Config::set('services.exa.key', 'k');
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'api.exa.ai/*' => $http->response(['results' => [['url' => 'https://zoon.ru/x']]], 200),
|
|
]);
|
|
expect((new ExaSiteFinder($http))->findSite('X', 'Красноярский край'))->toBeNull();
|
|
|
|
$http2 = app(HttpFactory::class);
|
|
$http2->fake(['api.exa.ai/*' => $http2->response('boom', 500)]);
|
|
expect((new ExaSiteFinder($http2))->findSite('Y', 'Красноярский край'))->toBeNull();
|
|
});
|
|
|
|
it('findSites: пул отдаёт домены по именам; чёрный список → null', function () {
|
|
Config::set('services.exa', ['key' => 'k', 'base_url' => 'https://api.exa.ai', 'timeout_sec' => 30, 'concurrency' => 5]);
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake(function ($request) use ($http) {
|
|
$q = (string) ($request->data()['query'] ?? '');
|
|
if (str_contains($q, 'CarMoney')) {
|
|
return $http->response(['results' => [['url' => 'https://www.carmoney.ru/about']]], 200);
|
|
}
|
|
if (str_contains($q, 'Пусто')) {
|
|
return $http->response(['results' => [['url' => 'https://zoon.ru/x']]], 200); // только каталог → null
|
|
}
|
|
if (str_contains($q, 'Cashmotor')) {
|
|
return $http->response(['results' => [['url' => 'https://cashmotor.ru/']]], 200);
|
|
}
|
|
|
|
return $http->response(['results' => []], 200);
|
|
});
|
|
|
|
$map = (new ExaSiteFinder($http))->findSites(['CarMoney', 'Пусто', 'Cashmotor'], 'Красноярский край');
|
|
|
|
expect($map)->toBe(['CarMoney' => 'carmoney.ru', 'Пусто' => null, 'Cashmotor' => 'cashmotor.ru']);
|
|
});
|
|
|
|
it('findSites: больше порции (concurrency=2) — все имена обработаны по чанкам', function () {
|
|
Config::set('services.exa', ['key' => 'k', 'base_url' => 'https://api.exa.ai', 'timeout_sec' => 30, 'concurrency' => 2]);
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake(function ($request) use ($http) {
|
|
$q = (string) ($request->data()['query'] ?? '');
|
|
preg_match('/Ф(\d)/u', $q, $m);
|
|
$i = $m[1] ?? '0';
|
|
|
|
return $http->response(['results' => [['url' => "https://firma{$i}.ru/"]]], 200);
|
|
});
|
|
|
|
$map = (new ExaSiteFinder($http))->findSites(['Ф1', 'Ф2', 'Ф3', 'Ф4', 'Ф5'], 'Красноярский край');
|
|
|
|
expect($map)->toHaveCount(5)
|
|
->and($map['Ф1'])->toBe('firma1.ru')
|
|
->and($map['Ф5'])->toBe('firma5.ru');
|
|
});
|
|
|
|
it('findSites: без ключа → все null', function () {
|
|
Config::set('services.exa.key', '');
|
|
|
|
$map = (new ExaSiteFinder(app(HttpFactory::class)))->findSites(['A', 'B'], 'Красноярский край');
|
|
|
|
expect($map)->toBe(['A' => null, 'B' => null]);
|
|
});
|
|
|
|
// EXA за Cloudflare режет российские IP → на боевом канал В ходит через зарубежный прокси
|
|
// (services.exa.proxy). Само проксирование — опция Guzzle, под Http::fake не наблюдается
|
|
// (перехват до отправки), поэтому проверяем, что добавление прокси НЕ ломает цепочку запроса
|
|
// и домен по-прежнему извлекается; фактический проход через прокси проверен вживую (curl→200).
|
|
it('с заданным proxy запрос не ломается — домен извлекается (findSite и findSites)', function () {
|
|
Config::set('services.exa', [
|
|
'key' => 'k',
|
|
'base_url' => 'https://api.exa.ai',
|
|
'timeout_sec' => 30,
|
|
'concurrency' => 5,
|
|
'proxy' => 'socks5h://127.0.0.1:10808',
|
|
]);
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'api.exa.ai/*' => $http->response(['results' => [['url' => 'https://carmoney.ru']]], 200),
|
|
]);
|
|
|
|
$f = new ExaSiteFinder($http);
|
|
|
|
expect($f->findSite('CarMoney', 'Красноярский край'))->toBe('carmoney.ru')
|
|
->and($f->findSites(['CarMoney'], 'Красноярский край'))->toBe(['CarMoney' => 'carmoney.ru']);
|
|
});
|