Files
portal/app/tests/Frontend/AdminIncidentDetailView.spec.ts
T
Дмитрий f94552d452 WIP чекпойнт: lead-region/supplier бэкенд + фронт-редизайн + Pint + тесты
92 файла одной пачкой. Исключены чужие зоны: CLAUDE.md, .claude/settings.json, docs/observer/.pii-counters.json.
gitleaks staged: no leaks found. Не верифицировано тестами - сохранение труда в историю.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 05:17:12 +03:00

187 lines
7.6 KiB
TypeScript
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.
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());
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<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 }),
);
vi.mocked(adminApi.notifyIncidentRkn).mockRejectedValue(new Error('РКН endpoint недоступен'));
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);
});
});