2026-05-17 06:29:46 +03:00
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
2026-05-09 04:17:17 +03:00
|
|
|
|
import { createVuetify } from 'vuetify';
|
|
|
|
|
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
|
|
|
|
import AdminIncidentsView from '../../resources/js/views/admin/AdminIncidentsView.vue';
|
2026-05-17 06:29:46 +03:00
|
|
|
|
import { ADMIN_INCIDENTS } from '../../resources/js/composables/mockAdmin';
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
vi.mocked(adminApi.listAdminIncidents).mockResolvedValue({
|
|
|
|
|
|
incidents: ADMIN_INCIDENTS.map((r) => ({
|
|
|
|
|
|
id: r.id,
|
|
|
|
|
|
incident_id: r.incident_id,
|
|
|
|
|
|
type: r.category as string,
|
|
|
|
|
|
severity: r.severity,
|
|
|
|
|
|
summary: r.title,
|
|
|
|
|
|
started_at: r.detected_at,
|
|
|
|
|
|
detected_at: r.detected_at,
|
|
|
|
|
|
resolved_at: null,
|
|
|
|
|
|
status: (r.status === 'closed' ? 'resolved' : r.status) as 'open' | 'investigating' | 'resolved',
|
|
|
|
|
|
affected_tenants_count: r.affected_tenants,
|
|
|
|
|
|
affected_users_count: null,
|
|
|
|
|
|
rkn_notified: r.rkn_notified,
|
|
|
|
|
|
rkn_notified_at: null,
|
|
|
|
|
|
rkn_deadline_at: r.rkn_deadline_at,
|
|
|
|
|
|
})),
|
|
|
|
|
|
total: ADMIN_INCIDENTS.length,
|
|
|
|
|
|
limit: 100,
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
summary: {
|
|
|
|
|
|
open: ADMIN_INCIDENTS.filter((r) => r.status === 'open').length,
|
|
|
|
|
|
investigating: ADMIN_INCIDENTS.filter((r) => r.status === 'investigating').length,
|
|
|
|
|
|
rkn_pending: ADMIN_INCIDENTS.filter(
|
|
|
|
|
|
(r) => ['pdn_breach', 'data_breach'].includes(r.category) && !r.rkn_notified,
|
|
|
|
|
|
).length,
|
|
|
|
|
|
total_unresolved: ADMIN_INCIDENTS.filter((r) => r.status !== 'resolved' && r.status !== 'closed').length,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-05-09 04:17:17 +03:00
|
|
|
|
|
|
|
|
|
|
const mountView = async () => {
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createMemoryHistory(),
|
2026-05-16 13:50:48 +03:00
|
|
|
|
routes: [
|
|
|
|
|
|
{ path: '/admin/incidents', name: 'admin-incidents', component: AdminIncidentsView },
|
|
|
|
|
|
{ path: '/admin/incidents/:id', name: 'admin-incident-detail', component: { template: '<div />' } },
|
|
|
|
|
|
],
|
2026-05-09 04:17:17 +03:00
|
|
|
|
});
|
|
|
|
|
|
await router.push('/admin/incidents');
|
|
|
|
|
|
await router.isReady();
|
2026-05-17 06:29:46 +03:00
|
|
|
|
const wrapper = mount(AdminIncidentsView, { global: { plugins: [createVuetify(), router] } });
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
return { wrapper, router };
|
2026-05-09 04:17:17 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
describe('AdminIncidentsView.vue', () => {
|
|
|
|
|
|
it('монтируется и содержит заголовок «Инциденты»', async () => {
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const { wrapper } = await mountView();
|
2026-05-09 04:17:17 +03:00
|
|
|
|
expect(wrapper.text()).toContain('Инциденты');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('содержит 3 stats: Открыто / Расследуется / РКН-уведомлений', async () => {
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const { wrapper } = await mountView();
|
2026-05-09 04:17:17 +03:00
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('Открыто');
|
|
|
|
|
|
expect(text).toContain('Расследуется');
|
|
|
|
|
|
expect(text).toContain('РКН-уведомлений');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('содержит фильтр-toggle по статусам (5 значений)', async () => {
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const { wrapper } = await mountView();
|
2026-05-09 04:17:17 +03:00
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('Все');
|
|
|
|
|
|
expect(text).toContain('Открыты');
|
|
|
|
|
|
expect(text).toContain('Решены');
|
|
|
|
|
|
expect(text).toContain('Закрыты');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('показывает PDN-breach с РКН pending chip', async () => {
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const { wrapper } = await mountView();
|
2026-05-09 04:17:17 +03:00
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('Утечка ПДн');
|
|
|
|
|
|
expect(text).toContain('РКН pending');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('содержит incident_id в формате INC-YYYY-MMDD-NNNN', async () => {
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const { wrapper } = await mountView();
|
2026-05-09 04:17:17 +03:00
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('INC-2026-0507-0034');
|
|
|
|
|
|
expect(text).toContain('INC-2026-0506-0028');
|
|
|
|
|
|
});
|
2026-05-16 13:50:48 +03:00
|
|
|
|
|
|
|
|
|
|
it('клик по строке инцидента вызывает router.push на admin-incident-detail', async () => {
|
|
|
|
|
|
const { wrapper, router } = await mountView();
|
|
|
|
|
|
const pushSpy = vi.spyOn(router, 'push');
|
2026-05-17 06:29:46 +03:00
|
|
|
|
// get first row — populated via API mock
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const vm = wrapper.vm as unknown as { rowsState: Array<{ id: number }> };
|
|
|
|
|
|
const firstId = vm.rowsState[0].id;
|
|
|
|
|
|
const row = wrapper.find(`[data-testid="incident-row-${firstId}"]`);
|
|
|
|
|
|
expect(row.exists()).toBe(true);
|
|
|
|
|
|
await row.trigger('click');
|
|
|
|
|
|
expect(pushSpy).toHaveBeenCalledWith({ name: 'admin-incident-detail', params: { id: firstId } });
|
|
|
|
|
|
});
|
2026-05-09 04:17:17 +03:00
|
|
|
|
});
|