152 lines
4.6 KiB
TypeScript
152 lines
4.6 KiB
TypeScript
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
|
import { screen, waitFor } from '@testing-library/react';
|
||
|
|
import userEvent from '@testing-library/user-event';
|
||
|
|
import MockAdapter from 'axios-mock-adapter';
|
||
|
|
import { apiClient } from '@/api/client';
|
||
|
|
import { TemplatePickerModal } from '@/components/TemplatePickerModal';
|
||
|
|
import { renderWithProviders } from './utils';
|
||
|
|
import type { SimulationTemplate } from '@/api/types';
|
||
|
|
|
||
|
|
const TEMPLATES: SimulationTemplate[] = [
|
||
|
|
{
|
||
|
|
id: 1,
|
||
|
|
name: 'Mimikatz LSASS Dump',
|
||
|
|
description: null,
|
||
|
|
commands: null,
|
||
|
|
prerequisites: null,
|
||
|
|
techniques: [{ id: 'T1003', name: 'OS Credential Dumping', tactics: [] }],
|
||
|
|
tactics: [],
|
||
|
|
created_at: '2026-05-28T00:00:00',
|
||
|
|
updated_at: null,
|
||
|
|
created_by: { id: 1, username: 'alice' },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 2,
|
||
|
|
name: 'PowerShell Empire',
|
||
|
|
description: null,
|
||
|
|
commands: null,
|
||
|
|
prerequisites: null,
|
||
|
|
techniques: [],
|
||
|
|
tactics: [{ id: 'TA0002', name: 'Execution' }],
|
||
|
|
created_at: '2026-05-28T01:00:00',
|
||
|
|
updated_at: null,
|
||
|
|
created_by: { id: 1, username: 'alice' },
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const onClose = vi.fn();
|
||
|
|
const onInstantiated = vi.fn();
|
||
|
|
const onSelectTemplate = vi.fn();
|
||
|
|
|
||
|
|
describe('TemplatePickerModal', () => {
|
||
|
|
let mock: MockAdapter;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
mock = new MockAdapter(apiClient);
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
mock.restore();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows loading state while fetching', () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(() => new Promise(() => {}));
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
expect(screen.getByTestId('loading-state')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows empty state when no templates', async () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(200, []);
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
expect(screen.getByText(/No templates available/i)).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('lists templates with name and MITRE count', async () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('template-picker-table')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('PowerShell Empire')).toBeInTheDocument();
|
||
|
|
// T1(1 tech) + 0 tactics = 1 for first, 0 tech + 1 tactic = 1 for second
|
||
|
|
expect(screen.getAllByText('1').length).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onSelectTemplate when a template row is clicked', async () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
await user.click(screen.getByTestId('template-row-1'));
|
||
|
|
expect(onSelectTemplate).toHaveBeenCalledWith(TEMPLATES[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onClose when Cancel is clicked', async () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
await user.click(screen.getByText('Cancel'));
|
||
|
|
expect(onClose).toHaveBeenCalledOnce();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows error state on fetch failure', async () => {
|
||
|
|
mock.onGet('/simulation-templates').reply(500, { error: 'Server error' });
|
||
|
|
renderWithProviders(
|
||
|
|
<TemplatePickerModal
|
||
|
|
engagementId={1}
|
||
|
|
onClose={onClose}
|
||
|
|
onInstantiated={onInstantiated}
|
||
|
|
onSelectTemplate={onSelectTemplate}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('error-state')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|