$overrides */ function seedGateRange(array $overrides = []): void { $importId = (int) DB::table('phone_ranges_imports')->insertGetId([ 'source_url' => 'https://rossvyaz.gov.ru/test', 'checksum_sha256' => str_repeat('b', 64), 'status' => 'completed', 'imported_at' => now(), ]); DB::table('phone_ranges')->insert(array_merge([ 'def_code' => 921, 'from_num' => 5550000, 'to_num' => 5559999, 'operator' => 'МегаФон', 'region' => 'Санкт-Петербург', 'subject_code' => 83, 'imported_at' => now(), 'import_id' => $importId, ], $overrides)); } /** * @param array $overrides * @return array{number:string,kind:string,label:string,url:?string,office:?string,tracker:bool} */ function gateCandidate(string $number, array $overrides = []): array { return array_merge([ 'number' => $number, 'kind' => 'text', 'label' => 'в тексте сайта — проверьте', 'url' => 'https://review-group.ru/', 'office' => null, 'tracker' => false, ], $overrides); } it('выкидывает номер, чей код города отсутствует в реестре Россвязи', function (): void { seedGateRange(); // реестр непустой (есть 921) // 76132306130 — код «613», такого в реестре нет (мусор со страницы-агрегатора). $out = app(PhoneValidityGate::class)->filter([gateCandidate('76132306130')]); expect($out)->toBe([]); }); it('оставляет номер, который есть в реестре Россвязи', function (): void { seedGateRange(); // def_code=921, 5550000..5559999 $out = app(PhoneValidityGate::class)->filter([ gateCandidate('79215555123', ['kind' => 'code', 'label' => 'в коде сайта']), ]); expect($out)->toHaveCount(1) ->and($out[0]['number'])->toBe('79215555123'); }); it('фильтрует только мусор, реальные соседи остаются', function (): void { seedGateRange(); $out = app(PhoneValidityGate::class)->filter([ gateCandidate('76132306130'), // мусор (код 613) gateCandidate('79215555123', ['kind' => 'code']), // реальный (921) gateCandidate('72279347817'), // мусор (код 227 — вообще <300, запрещён реестром) ]); expect($out)->toHaveCount(1) ->and($out[0]['number'])->toBe('79215555123'); }); it('при пустом реестре не режет ничего (fail-open)', function (): void { // seedGateRange НЕ вызван → phone_ranges пуст (как на dev). Фильтр не должен // выкосить всё из-за незагруженного реестра. $out = app(PhoneValidityGate::class)->filter([ gateCandidate('76132306130'), gateCandidate('79215555123'), ]); expect($out)->toHaveCount(2); }); it('никогда не трогает короткие номера «требует проверки» (их нельзя сверить)', function (): void { seedGateRange(); // 2713333 — локальный 7-значный, код города не определён (kind=uncertain). // Сверить с реестром нельзя → не теряем. $out = app(PhoneValidityGate::class)->filter([ gateCandidate('2713333', ['kind' => 'uncertain']), ]); expect($out)->toHaveCount(1) ->and($out[0]['number'])->toBe('2713333'); }); it('второй слой (ДаДата) снимает мусор, прошедший реестр', function (): void { seedGateRange(); // 79215555123 есть в реестре $dropAll = new class implements PhoneValidityLayer { public function isValid(string $number): bool { return false; // ДаДата сказала «мусор» } }; $gate = new PhoneValidityGate(new RossvyazPrefixLookup, $dropAll); $out = $gate->filter([gateCandidate('79215555123', ['kind' => 'code'])]); expect($out)->toBe([]); }); it('второй слой НЕ вызывается для номеров вне реестра (мусор отсёк слой 1 бесплатно)', function (): void { seedGateRange(); $spy = new class implements PhoneValidityLayer { public bool $called = false; public function isValid(string $number): bool { $this->called = true; return true; } }; $gate = new PhoneValidityGate(new RossvyazPrefixLookup, $spy); // 76132306130 — код 613, вне реестра → слой 1 выкидывает бесплатно, ДаДата не дёргается. $out = $gate->filter([gateCandidate('76132306130')]); expect($out)->toBe([]) ->and($spy->called)->toBeFalse(); });