Files
portal/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
T
Дмитрий 68c8eaead8 test(schema): обновить сторож метрик под v8.62 (автоподбор +4 таблицы)
SchemaDeltaTest жёстко ждал метрики v8.55 (74 таблицы/128 индексов/44 RLS),
но автоподбор добавил в schema.sql 4 таблицы (runs/competitors/sources/
merge_events) + 9 индексов + 4 RLS-политики → 78/137/48. Это был давно
красный сторож (не баг фичи). Обновлены ожидаемые числа + комментарий.

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

103 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Deal;
use App\Models\Tenant;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
// NOTE: \Tests\TestCase auto-binds via tests/Pest.php (->in('Feature')); explicit
// uses(\Tests\TestCase::class) conflicts ("already uses the test case").
// DatabaseTransactions — изоляция: каждый тест выполняется в транзакции, rollback после.
// Project convention: LeadChargeTest / PricingTierTest используют тот же паттерн.
uses(DatabaseTransactions::class);
it('tenants table has delivered_in_month column with CHECK >= 0', function () {
expect(Schema::hasColumn('tenants', 'delivered_in_month'))->toBeTrue();
DB::table('tenants')->where('id', '<', 0)->update(['delivered_in_month' => 5]); // no-op
expect(fn () => DB::statement(
'INSERT INTO tenants (subdomain, organization_name, contact_email, delivered_in_month) '.
"VALUES ('t-neg-test', 'X', 'x@x', -1)"
))->toThrow(QueryException::class);
});
it('lead_charges table has charge_source column with CHECK on prepaid=zero-price', function () {
expect(Schema::hasColumn('lead_charges', 'charge_source'))->toBeTrue();
$tenant = Tenant::factory()->create();
$deal = Deal::factory()->create(['tenant_id' => $tenant->id]);
expect(fn () => DB::table('lead_charges')->insert([
'tenant_id' => $tenant->id,
'deal_id' => $deal->id,
'deal_received_at' => $deal->received_at,
'tier_no' => 1,
'price_per_lead_kopecks' => 50000,
'charge_source' => 'prepaid',
'charged_at' => now(),
'created_at' => now(),
]))->toThrow(QueryException::class);
});
it('supplier_leads table has recovered_from_csv_at column', function () {
expect(Schema::hasColumn('supplier_leads', 'recovered_from_csv_at'))->toBeTrue();
});
it('supplier_csv_reconcile_log table exists with required columns and status CHECK', function () {
expect(Schema::hasTable('supplier_csv_reconcile_log'))->toBeTrue();
expect(Schema::hasColumns('supplier_csv_reconcile_log', [
'id', 'started_at', 'finished_at', 'window_start', 'window_end',
'total_csv_rows', 'matched_count', 'recovered_count', 'drift_ratio',
'status', 'error_message', 'alert_email_sent_at', 'created_at',
]))->toBeTrue();
expect(fn () => DB::table('supplier_csv_reconcile_log')->insert([
'started_at' => now(),
'window_start' => now()->subDay(),
'window_end' => now(),
'status' => 'unknown_status',
]))->toThrow(QueryException::class);
});
it('schema.sql v8.62 has correct metrics — 78 base tables, 137 indexes, 48 RLS policies', function () {
// Замена destructive `migrate:fresh` (cross-test coupling: после DROP CASCADE остальные
// Feature-тесты в той же сессии видели пустую БД). Static parse `db/schema.sql` —
// источник истины метрик.
// v8.21 (Sprint 4): +1 таблица import_unknown_statuses, +1 индекс, +1 RLS-политика.
// v8.22 (Plan 6/C9): +1 GIN-индекс idx_projects_regions.
// v8.25 (supplier-failover): +1 таблица supplier_manual_sync_queue, +2 индекса.
// v8.26 (project-migration-redesign Plans 1-3): +1 таблица project_supplier_links (M:N pivot)
// + 2 индекса (supplier_projects_platform_key_subject_unique, idx_psl_*).
// v8.30: +1 таблица scheduler_heartbeats (SaaS-level, hole #6).
// v8.31: 7 audit-таблиц переведены в PARTITION BY RANGE, hole #2.
// v8.35 (legacy webhook removal): 2 таблицы (webhook_log partitioned + rejected_deals_log)
// −5 индексов, −2 RLS-политики, −2 колонки tenants.webhook_token/webhook_token_rotated_at.
// v8.36→v8.52: рост схемы (lead-region phone_ranges/lead_region_resolution_log,
// project_routing_snapshots, tenant_requisites, support_requests и др.).
// v8.54 (Эпик 4 online-defer): +1 таблица supplier_deferred_sync (SaaS-level, PK неявный, +0 явных индексов).
// v8.55 (Эпик 5 отчёт заливки): +1 таблица supplier_sync_runs + 1 индекс idx_supplier_sync_runs_created.
// Статический парс после v8.54/v8.55 был: 74 base tables, 128 индексов, 44 RLS-политики.
// v8.58→v8.62 (Автоподбор конкурентов, ветка worktree-avtopodbor): +4 base таблицы
// (autopodbor_runs/competitors/sources/merge_events) + 9 индексов (вкл. 2 UNIQUE dedup) +
// 4 RLS-политики tenant_isolation. Итог статического парса db/schema.sql (v8.62):
// 78 base tables, 137 индексов, 48 RLS-политик.
// NB: бегущий счётчик в ШАПКЕ schema.sql несёт исторический дрейф — отдельный canon-sync,
// не предмет этого теста; тест сверяет фактический парс ФАЙЛА.
$schemaPath = dirname(base_path()).DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql';
expect(is_file($schemaPath) && is_readable($schemaPath))->toBeTrue();
$schema = file_get_contents($schemaPath);
expect($schema)->not->toBeFalse();
// 66 base tables = все CREATE TABLE минус PARTITION OF.
$createTables = preg_match_all('/^CREATE TABLE\b/m', $schema);
$partitionOf = preg_match_all('/CREATE TABLE\s+\w+\s+PARTITION OF\b/m', $schema);
$baseTables = $createTables - $partitionOf;
expect($baseTables)->toBe(78); // v8.62: +4 autopodbor таблицы к v8.55 (74)
$createIndexes = preg_match_all('/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/m', $schema);
expect($createIndexes)->toBe(137); // v8.62: +9 autopodbor индексов к v8.55 (128)
$createPolicies = preg_match_all('/^CREATE\s+POLICY\b/m', $schema);
expect($createPolicies)->toBe(48); // v8.62: +4 autopodbor RLS-политики к v8.52 (44)
});