2026-05-16 13:50:48 +03:00
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
|
|
|
|
|
import { createVuetify } from 'vuetify';
|
|
|
|
|
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
|
|
|
|
import AdminIncidentDetailView from '../../resources/js/views/admin/AdminIncidentDetailView.vue';
|
|
|
|
|
|
import type { ApiAdminIncidentDetail } 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,
|
|
|
|
|
|
getAdminIncidentDetail: vi.fn(),
|
|
|
|
|
|
notifyIncidentRkn: vi.fn(),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const adminApi = await import('../../resources/js/api/admin');
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function makeDetail(overrides: Partial<ApiAdminIncidentDetail> = {}): ApiAdminIncidentDetail {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: 7,
|
|
|
|
|
|
incident_id: 'INC-2026-0516-0007',
|
|
|
|
|
|
type: 'data_breach',
|
|
|
|
|
|
severity: 'high',
|
|
|
|
|
|
summary: 'Утечка данных тенантов',
|
|
|
|
|
|
root_cause: 'Неправильная RLS-политика',
|
|
|
|
|
|
postmortem_url: 'https://example.com/postmortem',
|
|
|
|
|
|
started_at: '2026-05-16T10:00:00Z',
|
|
|
|
|
|
detected_at: '2026-05-16T10:30:00Z',
|
|
|
|
|
|
resolved_at: null,
|
|
|
|
|
|
status: 'investigating',
|
|
|
|
|
|
affected_tenants: [
|
|
|
|
|
|
{ id: 1, organization_name: 'Окна Москва ООО' },
|
|
|
|
|
|
{ id: 2, organization_name: 'ИП Петров' },
|
|
|
|
|
|
],
|
|
|
|
|
|
affected_users_count: 42,
|
|
|
|
|
|
notification_sent_at: null,
|
|
|
|
|
|
rkn_notified: false,
|
|
|
|
|
|
rkn_notified_at: null,
|
|
|
|
|
|
rkn_deadline_at: '2026-05-17T10:30:00Z',
|
|
|
|
|
|
created_by_admin: 'admin@liderra.ru',
|
|
|
|
|
|
closed_by_admin: null,
|
|
|
|
|
|
created_at: '2026-05-16T10:35:00Z',
|
|
|
|
|
|
updated_at: '2026-05-16T10:35:00Z',
|
|
|
|
|
|
...overrides,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildRouter = (id: number) => {
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createMemoryHistory(),
|
|
|
|
|
|
routes: [
|
|
|
|
|
|
{ path: '/admin/incidents', name: 'admin-incidents', component: { template: '<div />' } },
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/admin/incidents/:id',
|
|
|
|
|
|
name: 'admin-incident-detail',
|
|
|
|
|
|
component: AdminIncidentDetailView,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
return router.push({ name: 'admin-incident-detail', params: { id } }).then(() => router);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const mountDetail = async (id: number) => {
|
|
|
|
|
|
const router = await buildRouter(id);
|
|
|
|
|
|
await router.isReady();
|
|
|
|
|
|
const wrapper = mount(AdminIncidentDetailView, {
|
|
|
|
|
|
global: {
|
|
|
|
|
|
plugins: [createVuetify(), router],
|
|
|
|
|
|
stubs: { teleport: true },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
return wrapper;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
describe('AdminIncidentDetailView.vue', () => {
|
|
|
|
|
|
it('вызывает getAdminIncidentDetail с id из route и рендерит summary/incident_id/severity', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(makeDetail());
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
expect(adminApi.getAdminIncidentDetail).toHaveBeenCalledWith(7);
|
|
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('INC-2026-0516-0007');
|
|
|
|
|
|
expect(text).toContain('Утечка данных тенантов');
|
|
|
|
|
|
expect(text).toContain('High');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('404 от API → data-testid="incident-not-found"', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockRejectedValue({
|
|
|
|
|
|
response: { status: 404 },
|
|
|
|
|
|
});
|
|
|
|
|
|
const wrapper = await mountDetail(999);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="incident-not-found"]').exists()).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('500 от API → data-testid="incident-fetch-error" + кнопка Повторить', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockRejectedValue({
|
|
|
|
|
|
response: { status: 500, data: { message: 'Backend error' } },
|
|
|
|
|
|
});
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="incident-fetch-error"]').exists()).toBe(true);
|
|
|
|
|
|
// retry button calls loadIncident
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(makeDetail());
|
2026-06-17 05:17:12 +03:00
|
|
|
|
const retryBtn = wrapper.find(
|
|
|
|
|
|
'[data-testid="incident-fetch-error"] button, [data-testid="incident-fetch-error"] .v-btn',
|
|
|
|
|
|
);
|
2026-05-16 13:50:48 +03:00
|
|
|
|
expect(retryBtn.exists()).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('data_breach + rkn_notified=false → data-testid="rkn-notify-btn" видна', async () => {
|
2026-06-17 05:17:12 +03:00
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(
|
|
|
|
|
|
makeDetail({ type: 'data_breach', rkn_notified: false }),
|
|
|
|
|
|
);
|
2026-05-16 13:50:48 +03:00
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="rkn-notify-btn"]').exists()).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('клик rkn-notify → confirm → вызывает notifyIncidentRkn, карточка обновляется (rkn_notified=true, кнопка исчезает)', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(
|
|
|
|
|
|
makeDetail({ type: 'data_breach', rkn_notified: false }),
|
|
|
|
|
|
);
|
2026-06-17 05:17:12 +03:00
|
|
|
|
const notified = makeDetail({
|
|
|
|
|
|
type: 'data_breach',
|
|
|
|
|
|
rkn_notified: true,
|
|
|
|
|
|
rkn_notified_at: '2026-05-16T11:00:00Z',
|
|
|
|
|
|
});
|
2026-05-16 13:50:48 +03:00
|
|
|
|
vi.mocked(adminApi.notifyIncidentRkn).mockResolvedValue(notified);
|
|
|
|
|
|
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
// open dialog via btn
|
|
|
|
|
|
await wrapper.find('[data-testid="rkn-notify-btn"]').trigger('click');
|
|
|
|
|
|
await wrapper.vm.$nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
// call confirmRkn directly via defineExpose
|
|
|
|
|
|
const vm = wrapper.vm as unknown as {
|
|
|
|
|
|
confirmRkn: () => Promise<void>;
|
|
|
|
|
|
incident: ApiAdminIncidentDetail | null;
|
|
|
|
|
|
rknDialog: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
await vm.confirmRkn();
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
|
|
expect(adminApi.notifyIncidentRkn).toHaveBeenCalledWith(7);
|
|
|
|
|
|
expect(vm.incident?.rkn_notified).toBe(true);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="rkn-notify-btn"]').exists()).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('type !== data_breach → кнопка РКН-notify отсутствует', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(
|
|
|
|
|
|
makeDetail({ type: 'service_outage', rkn_notified: false }),
|
|
|
|
|
|
);
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="rkn-notify-btn"]').exists()).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('rkn_notified=true → показывает "РКН уведомлён", кнопки нет', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(
|
|
|
|
|
|
makeDetail({ type: 'data_breach', rkn_notified: true, rkn_notified_at: '2026-05-17T08:00:00Z' }),
|
|
|
|
|
|
);
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
expect(wrapper.find('[data-testid="rkn-notify-btn"]').exists()).toBe(false);
|
|
|
|
|
|
expect(wrapper.text()).toContain('РКН уведомлён');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('ошибка от notifyIncidentRkn → data-testid="rkn-error" виден', async () => {
|
|
|
|
|
|
vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue(
|
|
|
|
|
|
makeDetail({ type: 'data_breach', rkn_notified: false }),
|
|
|
|
|
|
);
|
2026-06-17 05:17:12 +03:00
|
|
|
|
vi.mocked(adminApi.notifyIncidentRkn).mockRejectedValue(new Error('РКН endpoint недоступен'));
|
2026-05-16 13:50:48 +03:00
|
|
|
|
|
|
|
|
|
|
const wrapper = await mountDetail(7);
|
|
|
|
|
|
const vm = wrapper.vm as unknown as {
|
|
|
|
|
|
confirmRkn: () => Promise<void>;
|
|
|
|
|
|
rknError: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
await vm.confirmRkn();
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
await wrapper.vm.$nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('[data-testid="rkn-error"]').exists()).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|