181 lines
7.5 KiB
TypeScript
181 lines
7.5 KiB
TypeScript
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);
|
||
});
|
||
});
|