Files
portal/app/tests/Feature/AdminIncidentsIndexTest.php
T
Дмитрий 56195c8a59 fix(backend): tenant middleware на auth-routes + HasPasswordRules trait + test password (audit P0-01 + O-refactor-03 + P2-01)
Закрытие аудита 2026-05-09 (b6ae8dd):
- P0-01: применён 'tenant' middleware (alias уже в bootstrap/app.php:17) к 3 auth:sanctum-группам:
  /api/notifications, /api/reminders, /api/reports/jobs (web.php:44/52/63).
  /api/deals и /api/admin/* остаются без auth (P1-10/Б-1) — в реестр Спринта 1 Phase F.
- O-refactor-03: HasPasswordRules trait извлекает rules + messages, подключён в Login/Register.
- P2-01: bcrypt('test') → bcrypt('test1234') в AdminIncidentsIndexTest (≥8 chars).
- bonus-fix: SetTenantContext::resolveTenantId — property_exists() заменён на isset() для
  Eloquent magic-attributes (auth-путь резолюции tenant_id никогда не работал из-за этого
  бага; тесты-смоки middleware покрывали только X-Tenant-Id header / subdomain). Без фикса
  P0-01 ломает 58 тестов в /api/notifications + /api/reminders + /api/reports/jobs.

Pest: 416/416 PASS.
Larastan: 0 errors.
Pint: clean.

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

169 lines
6.3 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;
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',
'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);
});