c6d2df908a
PhoneRangesImportCommand now resolves subject_code via RussianRegions::canonicalRegionName (pipe segment + обл./alias normalization) and persists region_normalized. messy.csv fixture covers real prod formats (3-digit DEF codes per chk_phone_ranges_def_code). 5/5 command tests GREEN.
111 lines
5.5 KiB
PHP
111 lines
5.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
uses(SharesSupplierPdo::class);
|
||
|
||
function rossvyazFixture(): string
|
||
{
|
||
return base_path('tests/Fixtures/rossvyaz/sample.csv');
|
||
}
|
||
|
||
it('dry-run parses csv, maps regions to subject_code, builds staging, does not swap', function (): void {
|
||
$this->artisan('phone-ranges:import', ['--file' => rossvyazFixture(), '--dry-run' => true])
|
||
->assertSuccessful();
|
||
|
||
// Staging построен (dry-run не свапает и не дропает staging — данные видны в той же tx).
|
||
expect(DB::table('phone_ranges_staging')->count())->toBe(3);
|
||
|
||
$r495 = DB::selectOne('SELECT subject_code FROM phone_ranges_staging WHERE def_code = 495');
|
||
$r921 = DB::selectOne('SELECT subject_code FROM phone_ranges_staging WHERE def_code = 921');
|
||
$r999 = DB::selectOne('SELECT subject_code FROM phone_ranges_staging WHERE def_code = 999');
|
||
|
||
expect((int) $r495->subject_code)->toBe(82) // Москва
|
||
->and((int) $r921->subject_code)->toBe(83) // Санкт-Петербург
|
||
->and($r999->subject_code)->toBeNull(); // Атлантида — не маппится
|
||
|
||
// Живой phone_ranges не тронут (свапа не было).
|
||
expect(DB::table('phone_ranges')->count())->toBe(0);
|
||
|
||
// Журнал импорта: dry-run → rolled_back, несматчившийся регион в error.
|
||
$imp = DB::table('phone_ranges_imports')->orderByDesc('id')->first();
|
||
expect($imp->status)->toBe('rolled_back')
|
||
->and($imp->error)->toContain('Атлантида');
|
||
});
|
||
|
||
it('maps all matched rows and counts unmatched separately', function (): void {
|
||
$this->artisan('phone-ranges:import', ['--file' => rossvyazFixture(), '--dry-run' => true])
|
||
->assertSuccessful();
|
||
|
||
$matched = DB::table('phone_ranges_staging')->whereNotNull('subject_code')->count();
|
||
$unmatched = DB::table('phone_ranges_staging')->whereNull('subject_code')->count();
|
||
|
||
expect($matched)->toBe(2)->and($unmatched)->toBe(1);
|
||
});
|
||
|
||
it('skips swap when checksum matches a completed import (idempotency)', function (): void {
|
||
$checksum = hash_file('sha256', rossvyazFixture());
|
||
DB::table('phone_ranges_imports')->insert([
|
||
'source_url' => 'https://rossvyaz.gov.ru/prev',
|
||
'checksum_sha256' => $checksum,
|
||
'status' => 'completed',
|
||
'imported_at' => now(),
|
||
'completed_at' => now(),
|
||
]);
|
||
|
||
// Не dry-run: но checksum совпал с completed → короткое замыкание ДО свапа.
|
||
$this->artisan('phone-ranges:import', ['--file' => rossvyazFixture()])
|
||
->assertSuccessful();
|
||
|
||
expect(DB::table('phone_ranges')->count())->toBe(0); // свапа не было
|
||
|
||
$latest = DB::table('phone_ranges_imports')->orderByDesc('id')->first();
|
||
expect($latest->status)->toBe('rolled_back');
|
||
});
|
||
|
||
it('force flag bypasses idempotency note even with matching checksum', function (): void {
|
||
// С --dry-run + --force: идемпотентность игнорируется, но dry-run всё равно не свапает.
|
||
$checksum = hash_file('sha256', rossvyazFixture());
|
||
DB::table('phone_ranges_imports')->insert([
|
||
'source_url' => 'https://rossvyaz.gov.ru/prev',
|
||
'checksum_sha256' => $checksum,
|
||
'status' => 'completed',
|
||
'imported_at' => now(),
|
||
'completed_at' => now(),
|
||
]);
|
||
|
||
$this->artisan('phone-ranges:import', ['--file' => rossvyazFixture(), '--dry-run' => true, '--force' => true])
|
||
->assertSuccessful();
|
||
|
||
// --force обошёл idempotency → staging построен заново (3 строки), но dry-run не свапнул.
|
||
expect(DB::table('phone_ranges_staging')->count())->toBe(3);
|
||
expect(DB::table('phone_ranges')->count())->toBe(0);
|
||
});
|
||
|
||
it('normalizes real Россвязь region formats to subject_code and fills region_normalized', function (): void {
|
||
// Форматы из реального прод-реестра (топ unmapped 02.06.2026): префикс «г. »,
|
||
// pipe-сегмент региона, сокращение «обл.», перевёрнутая «Республика Удмуртская»,
|
||
// и безнадёжный city-only «г.о. Тольятти». def-коды 3-значные (chk_phone_ranges_def_code 300-999).
|
||
$this->artisan('phone-ranges:import', ['--file' => base_path('tests/Fixtures/rossvyaz/messy.csv'), '--dry-run' => true])
|
||
->assertSuccessful();
|
||
|
||
$moscow = DB::selectOne('SELECT subject_code, region_normalized FROM phone_ranges_staging WHERE def_code = 495');
|
||
$orenburg = DB::selectOne('SELECT subject_code, region_normalized FROM phone_ranges_staging WHERE def_code = 922');
|
||
$udmurtia = DB::selectOne('SELECT subject_code, region_normalized FROM phone_ranges_staging WHERE def_code = 987');
|
||
$togliatti = DB::selectOne('SELECT subject_code, region_normalized FROM phone_ranges_staging WHERE def_code = 902');
|
||
|
||
expect((int) $moscow->subject_code)->toBe(82)
|
||
->and($moscow->region_normalized)->toBe('Москва')
|
||
->and((int) $orenburg->subject_code)->toBe(62)
|
||
->and($orenburg->region_normalized)->toBe('Оренбургская область')
|
||
->and((int) $udmurtia->subject_code)->toBe(21)
|
||
->and($udmurtia->region_normalized)->toBe('Удмуртская Республика')
|
||
->and($togliatti->subject_code)->toBeNull()
|
||
->and($togliatti->region_normalized)->toBeNull();
|
||
});
|