14dc317e2b
Чтение incidents_log с фильтрами type/severity/unresolved_only + summary
(open/investigating/rkn_pending/total_unresolved).
Backend (AdminIncidentsController::index):
- ORDER BY started_at DESC. Filters: type, severity, unresolved_only=true.
- Derived: incident_id (INC-YYYY-MMDD-NNNN), status (resolved_at!=null →
resolved; detected_at!=null → investigating; иначе open),
affected_tenants_count из BIGINT[] (parsePgArray для '{1,2,3}'),
rkn_deadline_at = detected_at+24h для data_breach без notification.
- summary: open/investigating/rkn_pending/total_unresolved.
Pest +11 (AdminIncidentsIndexTest):
- пустой / incident_id формат / derive status / filter type+severity /
unresolved_only / ORDER BY started_at DESC / rkn_deadline +24h для
data_breach / non-data_breach без deadline / summary.rkn_pending /
limit+offset.
- Quirk: saas_admin_users.full_name (не first/last) + нет updated_at.
Frontend:
- api/admin.ts::listAdminIncidents — типизированный helper.
- AdminIncidentsView: унифицированный IncidentRow (mock-category ↔
API-type, mock-title ↔ API-summary). Reactive rowsState+stats default
= MOCK; loadIncidents() async на onMounted; fetchError + warning
alert + MOCK fallback; reload-btn. РКН pending chip учитывает оба
pdn_breach/data_breach.
Vitest +5:
- listAdminIncidents на mount / replace state+stats + rkn_deadline /
reject → fetchError+alert+fallback / reload-btn x2 / РКН pending chip
виден для data_breach без notification.
PHPStan baseline регенерирован. cspell-glossary +MMDD.
Регресс:
- Lint+type-check+format passed.
- Vitest 305/305 за 20.59 сек (+5 от 300).
- Vite build 1.05 сек.
- Pint + PHPStan passed.
- Pest 248/248 за 28.02 сек (+11 от 237, 951 assertion).
Реестр v1.67→v1.68 / CLAUDE.md v1.58→v1.59.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import AdminIncidentsView from '../../resources/js/views/admin/AdminIncidentsView.vue';
|
|
import type { ApiAdminIncident } from '../../resources/js/api/admin';
|
|
|
|
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
|
|
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
|
|
return {
|
|
...orig,
|
|
listAdminIncidents: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const adminApi = await import('../../resources/js/api/admin');
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function makeApiIncident(overrides: Partial<ApiAdminIncident> = {}): ApiAdminIncident {
|
|
return {
|
|
id: 1,
|
|
incident_id: 'INC-2026-0509-0001',
|
|
type: 'service_outage',
|
|
severity: 'medium',
|
|
summary: 'Test incident',
|
|
started_at: new Date().toISOString(),
|
|
detected_at: new Date().toISOString(),
|
|
resolved_at: null,
|
|
status: 'investigating',
|
|
affected_tenants_count: 0,
|
|
affected_users_count: null,
|
|
rkn_notified: false,
|
|
rkn_notified_at: null,
|
|
rkn_deadline_at: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const mountView = () =>
|
|
mount(AdminIncidentsView, {
|
|
global: { plugins: [createVuetify()] },
|
|
});
|
|
|
|
describe('AdminIncidentsView ↔ GET /api/admin/incidents integration', () => {
|
|
it('listAdminIncidents вызывается на mount', async () => {
|
|
vi.mocked(adminApi.listAdminIncidents).mockResolvedValueOnce({
|
|
incidents: [],
|
|
total: 0,
|
|
limit: 100,
|
|
offset: 0,
|
|
summary: { open: 0, investigating: 0, rkn_pending: 0, total_unresolved: 0 },
|
|
});
|
|
|
|
mountView();
|
|
await flushPromises();
|
|
expect(adminApi.listAdminIncidents).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('успех — replace rowsState + summary', async () => {
|
|
vi.mocked(adminApi.listAdminIncidents).mockResolvedValueOnce({
|
|
incidents: [
|
|
makeApiIncident({
|
|
id: 100,
|
|
type: 'data_breach',
|
|
severity: 'critical',
|
|
summary: 'PDN leak',
|
|
rkn_notified: false,
|
|
rkn_deadline_at: '2026-05-10T00:00:00Z',
|
|
}),
|
|
makeApiIncident({
|
|
id: 101,
|
|
type: 'service_outage',
|
|
summary: 'API timeout',
|
|
status: 'investigating',
|
|
}),
|
|
],
|
|
total: 2,
|
|
limit: 100,
|
|
offset: 0,
|
|
summary: { open: 0, investigating: 2, rkn_pending: 1, total_unresolved: 2 },
|
|
});
|
|
|
|
const wrapper = mountView();
|
|
await flushPromises();
|
|
|
|
const vm = wrapper.vm as unknown as {
|
|
rowsState: Array<{ id: number; title: string; category: string; rkn_deadline_at: string | null }>;
|
|
stats: { open: number; investigating: number; rkn_pending: number };
|
|
};
|
|
expect(vm.rowsState).toHaveLength(2);
|
|
expect(vm.rowsState[0].title).toBe('PDN leak');
|
|
expect(vm.rowsState[0].category).toBe('data_breach');
|
|
expect(vm.rowsState[0].rkn_deadline_at).toBe('2026-05-10T00:00:00Z');
|
|
expect(vm.stats.investigating).toBe(2);
|
|
expect(vm.stats.rkn_pending).toBe(1);
|
|
});
|
|
|
|
it('reject → fetchError=true + alert виден + MOCK fallback', async () => {
|
|
vi.mocked(adminApi.listAdminIncidents).mockRejectedValueOnce(new Error('500'));
|
|
|
|
const wrapper = mountView();
|
|
await flushPromises();
|
|
|
|
const vm = wrapper.vm as unknown as { fetchError: boolean; rowsState: unknown[] };
|
|
expect(vm.fetchError).toBe(true);
|
|
expect(vm.rowsState.length).toBeGreaterThan(0);
|
|
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
|
|
});
|
|
|
|
it('reload-btn вызывает listAdminIncidents второй раз', async () => {
|
|
vi.mocked(adminApi.listAdminIncidents).mockResolvedValue({
|
|
incidents: [],
|
|
total: 0,
|
|
limit: 100,
|
|
offset: 0,
|
|
summary: { open: 0, investigating: 0, rkn_pending: 0, total_unresolved: 0 },
|
|
});
|
|
|
|
const wrapper = mountView();
|
|
await flushPromises();
|
|
expect(adminApi.listAdminIncidents).toHaveBeenCalledTimes(1);
|
|
|
|
await wrapper.find('[data-testid="reload-btn"]').trigger('click');
|
|
await flushPromises();
|
|
expect(adminApi.listAdminIncidents).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('РКН pending chip виден для data_breach без rkn_notified', async () => {
|
|
vi.mocked(adminApi.listAdminIncidents).mockResolvedValueOnce({
|
|
incidents: [
|
|
makeApiIncident({
|
|
id: 200,
|
|
type: 'data_breach',
|
|
summary: 'breach',
|
|
rkn_notified: false,
|
|
}),
|
|
],
|
|
total: 1,
|
|
limit: 100,
|
|
offset: 0,
|
|
summary: { open: 0, investigating: 1, rkn_pending: 1, total_unresolved: 1 },
|
|
});
|
|
|
|
const wrapper = mountView();
|
|
await flushPromises();
|
|
expect(wrapper.text()).toContain('РКН pending');
|
|
});
|
|
});
|