Files
portal/app/tests/Feature/AdminIncidentsIndexTest.php
T
Дмитрий 23f81bdaf3 test: починка харнеса AdminBilling и AdminIncidents
AdminBillingIndexTest: teardown глушит session-триггеры на время очистки.
DELETE tenants каскадил в append-only tenant_operations_log, триггер
audit_block_mutation давал RAISE EXCEPTION. Плюс ensureRange гарантирует
месячные партиции balance_transactions за прошлые 2 месяца под SharesSupplierPdo.

AdminIncidentsIndexTest: добавлен трейт SharesSupplierPdo. Контроллер читает
через pgsql_supplier, тест писал через дефолтный pgsql под DatabaseTransactions,
cross-connection невидимость давала total=0.

Verify: оба класса 20 из 20 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:05:06 +03:00

174 lines
6.6 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 Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
// AdminIncidentsController читает через DB::connection('pgsql_supplier'); под
// DatabaseTransactions записи дефолтного pgsql невидимы отдельному PDO supplier'а
// → total=0. Трейт шарит один PDO между коннектами (откат сохраняется).
uses(SharesSupplierPdo::class);
beforeEach(function () {
DB::table('incidents_log')->delete();
// Создаём минимальный saas_admin_user для FK.
$this->adminId = (int) DB::table('saas_admin_users')->insertGetId([
'email' => 'admin-'.bin2hex(random_bytes(3)).'@test',
'full_name' => 'Admin Test',
'password_hash' => bcrypt('test1234'),
'is_active' => true,
'role' => 'support',
'created_at' => now(),
]);
});
function makeIncident(int $adminId, array $overrides = []): int
{
$started = $overrides['started_at'] ?? now();
$detected = $overrides['detected_at'] ?? $started;
return (int) DB::table('incidents_log')->insertGetId(array_merge([
'type' => 'service_outage',
'severity' => 'medium',
'started_at' => $started,
'detected_at' => $detected,
'resolved_at' => null,
'summary' => 'Test incident',
'created_by_admin_id' => $adminId,
'created_at' => now(),
], $overrides));
}
test('GET /api/admin/incidents 200 + пустой', function () {
$r = $this->getJson('/api/admin/incidents');
$r->assertStatus(200);
expect($r->json('incidents'))->toBe([]);
expect($r->json('total'))->toBe(0);
expect($r->json('summary.total_unresolved'))->toBe(0);
});
test('GET /api/admin/incidents возвращает ID + поля', function () {
$id = makeIncident($this->adminId, [
'type' => 'service_outage',
'severity' => 'high',
'summary' => 'API timeout — рост 502',
]);
$r = $this->getJson('/api/admin/incidents');
expect($r->json('total'))->toBe(1);
$row = $r->json('incidents.0');
expect($row['id'])->toBe($id);
expect($row['type'])->toBe('service_outage');
expect($row['severity'])->toBe('high');
expect($row['summary'])->toBe('API timeout — рост 502');
expect($row['incident_id'])->toMatch('/^INC-\d{4}-\d{4}-\d{4}$/');
});
test('GET /api/admin/incidents derive статус: resolved/investigating', function () {
makeIncident($this->adminId, ['summary' => 'Inv 1']); // detected_at=started → investigating
makeIncident($this->adminId, [
'summary' => 'Resolved 1',
'resolved_at' => now()->addHour(),
]);
$r = $this->getJson('/api/admin/incidents');
$byStatus = collect($r->json('incidents'))->groupBy('status');
expect($byStatus->get('investigating'))->toHaveCount(1);
expect($byStatus->get('resolved'))->toHaveCount(1);
});
test('GET /api/admin/incidents фильтр по type', function () {
makeIncident($this->adminId, ['type' => 'service_outage', 'summary' => 'A']);
makeIncident($this->adminId, ['type' => 'data_breach', 'summary' => 'B']);
makeIncident($this->adminId, ['type' => 'billing_failure', 'summary' => 'C']);
$r = $this->getJson('/api/admin/incidents?type=data_breach');
expect($r->json('total'))->toBe(1);
expect($r->json('incidents.0.summary'))->toBe('B');
});
test('GET /api/admin/incidents фильтр по severity', function () {
makeIncident($this->adminId, ['severity' => 'critical', 'summary' => 'crit']);
makeIncident($this->adminId, ['severity' => 'medium', 'summary' => 'med']);
$r = $this->getJson('/api/admin/incidents?severity=critical');
expect($r->json('total'))->toBe(1);
expect($r->json('incidents.0.summary'))->toBe('crit');
});
test('GET /api/admin/incidents unresolved_only=true фильтрует только нерешённые', function () {
makeIncident($this->adminId, ['summary' => 'open']);
makeIncident($this->adminId, [
'summary' => 'closed',
'resolved_at' => now()->addHour(),
]);
$r = $this->getJson('/api/admin/incidents?unresolved_only=true');
expect($r->json('total'))->toBe(1);
expect($r->json('incidents.0.summary'))->toBe('open');
});
test('GET /api/admin/incidents сортирует по started_at DESC', function () {
makeIncident($this->adminId, ['summary' => 'old', 'started_at' => now()->subDays(5)]);
makeIncident($this->adminId, ['summary' => 'new', 'started_at' => now()->subMinutes(5)]);
makeIncident($this->adminId, ['summary' => 'mid', 'started_at' => now()->subDays(1)]);
$r = $this->getJson('/api/admin/incidents');
$summaries = collect($r->json('incidents'))->pluck('summary')->all();
expect($summaries)->toBe(['new', 'mid', 'old']);
});
test('GET /api/admin/incidents data_breach без rkn_notified_at имеет deadline +24ч', function () {
$detected = now()->subHours(2);
makeIncident($this->adminId, [
'type' => 'data_breach',
'detected_at' => $detected,
'started_at' => $detected,
'summary' => 'PDN leak',
]);
$r = $this->getJson('/api/admin/incidents');
$row = $r->json('incidents.0');
expect($row['rkn_notified'])->toBeFalse();
expect($row['rkn_deadline_at'])->toBeString();
});
test('GET /api/admin/incidents non-data_breach НЕ имеет rkn_deadline', function () {
makeIncident($this->adminId, ['type' => 'service_outage']);
$r = $this->getJson('/api/admin/incidents');
expect($r->json('incidents.0.rkn_deadline_at'))->toBeNull();
});
test('GET /api/admin/incidents summary.rkn_pending считает PDN-breach без notification', function () {
// 2 PDN-breach: одна с notification, одна без
makeIncident($this->adminId, [
'type' => 'data_breach',
'rkn_notified_at' => now(),
'summary' => 'breach1 notified',
]);
makeIncident($this->adminId, [
'type' => 'data_breach',
'summary' => 'breach2 pending',
]);
// service_outage не считается
makeIncident($this->adminId, ['type' => 'service_outage', 'summary' => 'outage']);
$r = $this->getJson('/api/admin/incidents');
expect($r->json('summary.rkn_pending'))->toBe(1);
});
test('GET /api/admin/incidents limit + offset', function () {
foreach (range(1, 5) as $i) {
makeIncident($this->adminId, ['summary' => 'I'.$i, 'started_at' => now()->subMinutes($i)]);
}
$r = $this->getJson('/api/admin/incidents?limit=2&offset=1');
expect($r->json('total'))->toBe(5);
expect(count($r->json('incidents')))->toBe(2);
});