Files
portal/app/tests/Feature/Integration/SchemaV8_18Test.php
T
Дмитрий fb55bfdd1f feat(db): supplier_leads + projects.delivered_today + 2 system_settings (v8.18)
Plan 2/5 Task 1 — слой данных для supplier-webhook flow.

- supplier_leads (SaaS-level, без RLS) — raw payload incoming webhook'ов
- projects.delivered_today — дневной счётчик для проверки daily quota
- system_settings: supplier_webhook_secret + supplier_ip_allowlist

Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §5-§6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:07:31 +03:00

95 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
test('supplier_leads table exists with required columns', function () {
expect(Schema::hasTable('supplier_leads'))->toBeTrue();
expect(Schema::hasColumns('supplier_leads', [
'id',
'supplier_project_id',
'platform',
'raw_payload',
'vid',
'phone',
'received_at',
'source',
'processed_at',
'deals_created_count',
]))->toBeTrue();
});
test('supplier_leads has FK to supplier_projects with ON DELETE SET NULL', function () {
$row = DB::selectOne(
"SELECT c.confdeltype
FROM pg_constraint c
JOIN pg_class t ON c.conrelid = t.oid
JOIN pg_class r ON c.confrelid = r.oid
WHERE t.relname = 'supplier_leads'
AND r.relname = 'supplier_projects'
AND c.contype = 'f'"
);
expect($row)->not->toBeNull();
// confdeltype: 'a'=NO ACTION, 'r'=RESTRICT, 'c'=CASCADE, 'n'=SET NULL, 'd'=SET DEFAULT
expect($row->confdeltype)->toBe('n');
});
test('supplier_leads.source has CHECK enum', function () {
// Valid source: должно пройти.
$insertedId = DB::table('supplier_leads')->insertGetId([
'platform' => 'B1',
'raw_payload' => json_encode(['test' => 'valid_source']),
'vid' => 1000001,
'phone' => '+79991234567',
'source' => 'webhook',
]);
expect($insertedId)->toBeGreaterThan(0);
DB::table('supplier_leads')->where('id', $insertedId)->delete();
// Invalid source: должно отклониться CHECK constraint'ом.
expect(fn () => DB::table('supplier_leads')->insert([
'platform' => 'B1',
'raw_payload' => json_encode(['test' => 'invalid']),
'vid' => 1000002,
'phone' => '+79991234568',
'source' => 'invalid_source',
]))->toThrow(QueryException::class);
});
test('projects.delivered_today exists with default 0', function () {
expect(Schema::hasColumn('projects', 'delivered_today'))->toBeTrue();
$col = DB::selectOne(
"SELECT column_default
FROM information_schema.columns
WHERE table_name = 'projects' AND column_name = 'delivered_today'"
);
expect($col)->not->toBeNull();
expect($col->column_default)->toContain('0');
});
test('system_settings seed rows exist for supplier_webhook_secret + supplier_ip_allowlist', function () {
$secret = DB::table('system_settings')
->where('key', 'supplier_webhook_secret')
->first();
expect($secret)->not->toBeNull();
expect($secret->type)->toBe('string');
// Placeholder __SET_ON_DEPLOY__ (17 chars) валиден; реальный secret (≥32 chars) будет SET ON DEPLOY.
// Лимит 16 ловит пустую строку и слишком короткие случайные значения, но пропускает наш placeholder.
expect(strlen($secret->value))->toBeGreaterThanOrEqual(16);
$allowlist = DB::table('system_settings')
->where('key', 'supplier_ip_allowlist')
->first();
expect($allowlist)->not->toBeNull();
expect($allowlist->type)->toBe('json');
$decoded = json_decode($allowlist->value, true);
expect($decoded)->toBeArray();
});