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(); return { ...orig, getAdminIncidentDetail: vi.fn(), notifyIncidentRkn: vi.fn(), }; }); const adminApi = await import('../../resources/js/api/admin'); beforeEach(() => { vi.clearAllMocks(); }); function makeDetail(overrides: Partial = {}): 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: '
' } }, { 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()); const retryBtn = wrapper.find( '[data-testid="incident-fetch-error"] button, [data-testid="incident-fetch-error"] .v-btn', ); expect(retryBtn.exists()).toBe(true); }); it('data_breach + rkn_notified=false → data-testid="rkn-notify-btn" видна', async () => { vi.mocked(adminApi.getAdminIncidentDetail).mockResolvedValue( makeDetail({ type: 'data_breach', rkn_notified: false }), ); 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 }), ); const notified = makeDetail({ type: 'data_breach', rkn_notified: true, rkn_notified_at: '2026-05-16T11:00:00Z', }); 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; 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 }), ); vi.mocked(adminApi.notifyIncidentRkn).mockRejectedValue(new Error('РКН endpoint недоступен')); const wrapper = await mountDetail(7); const vm = wrapper.vm as unknown as { confirmRkn: () => Promise; rknError: string; }; await vm.confirmRkn(); await flushPromises(); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="rkn-error"]').exists()).toBe(true); }); });