Files
portal/app/tests/Feature/Autopodbor/Study/SiteOpenerValidityTest.php
T
Дмитрий a028af4bc8
Accessibility (Pa11y live) / a11y (push) Waiting to run
SAST — Semgrep / Semgrep SAST scan (push) Waiting to run
feat(autopodbor): сторож валидности телефонов на шаге 2 (реестр Россвязи + ДаДата)
Экстрактор кода страницы ловил любые 11 цифр (7/8 + телефонная группировка) без
проверки, что это настоящий российский номер — со страниц-агрегаторов пролезал мусор
с несуществующими кодами (227/303/453/613/761) и подписывался «городской».

PhoneValidityGate между CandidateBuilder и SourceAggregator:
- слой 1 (бесплатно, всегда): реестр Россвязи (phone_ranges) — нет в реестре → выкидываем;
- слой 2 (платно, за флагом autopodbor.phone_dadata_enabled, по умолчанию ВЫКЛ):
  DaDataValidityLayer перепроверяет прошедшие реестр номера, снимает qc 2/7,
  под дневным бюджетом + кэш вердикта; платим только за выживших (мусор отсекается ДО ДаДаты).
- fail-open при пустом реестре / сбое БД (иначе внешний catch в SiteOpener съел бы все номера);
- короткие/uncertain номера не трогаем (нельзя сверить).

Закрыты ОБА пути шага 2: обычный (RealCompetitorAgent) и глубокий (SiteOpener).
Миграций нет — реестр уже на проде (453k диапазонов).

Тесты: PhoneValidityGateTest (7), DaDataValidityLayerTest (6), SiteOpenerValidityTest (1);
весь набор автоподбора 519 зелёных, Pint + Larastan чисто.

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

68 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Autopodbor\Agent\Fetch\FetchedSite;
use App\Services\Autopodbor\Agent\Fetch\Fetcher;
use App\Services\Autopodbor\Agent\Study\SiteOpener;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
function seedOpenerRange(): void
{
$importId = (int) DB::table('phone_ranges_imports')->insertGetId([
'source_url' => 'https://rossvyaz.gov.ru/test',
'checksum_sha256' => str_repeat('c', 64),
'status' => 'completed',
'imported_at' => now(),
]);
DB::table('phone_ranges')->insert([
'def_code' => 391,
'from_num' => 2000000,
'to_num' => 2999999,
'operator' => 'Ростелеком',
'region' => 'Красноярский край',
'subject_code' => 24,
'imported_at' => now(),
'import_id' => $importId,
]);
}
/** Fetcher, отдающий фиксированный HTML. */
function openerFetcher(string $html): Fetcher
{
return new class($html) implements Fetcher
{
public function __construct(private string $html) {}
public function site(string $url): FetchedSite
{
return new FetchedSite($url, $this->html);
}
public function directory(string $url): array
{
return [];
}
};
}
it('глубокий путь: мусорный номер (код вне реестра) не попадает в numbers', function (): void {
seedOpenerRange(); // есть только 391 2000000..2999999
// Настоящий 73912920000 (391) + мусор 76132306130 (код 613, вне реестра).
$html = '<a href="tel:+73912920000">звонок</a>'
.' <span>+7 613 230 61 30</span>';
$r = (new SiteOpener(openerFetcher($html)))->open('kraslombard24.ru', '391');
$numbers = collect($r->numbers)->pluck('number');
expect($numbers)->toContain('73912920000')
->and($numbers)->not->toContain('76132306130');
});