2026-05-09 09:38:34 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
|
|
uses(DatabaseTransactions::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',
|
2026-05-09 18:22:30 +03:00
|
|
|
|
'password_hash' => bcrypt('test1234'),
|
2026-05-09 09:38:34 +03:00
|
|
|
|
'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);
|
|
|
|
|
|
});
|