2026-06-01 07:18:23 +03:00
|
|
|
|
<?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);
|
|
|
|
|
|
});
|
2026-06-02 15:37:35 +03:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|