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); });