Files
mimic/frontend/tests/TemplatePickerModal.test.tsx
Knacky 90fc5bab6c feat(frontend): sprint 5 — templates CRUD pages + nav + picker modal + dropdown
- types.ts: SimulationTemplate, SimulationTemplateCreateInput, SimulationTemplatePatchInput,
  extend SimulationCreateInput with template_id
- api/templates.ts: listTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate
- hooks/useTemplates.ts: useTemplates, useTemplate, useCreateTemplate, useUpdateTemplate,
  useDeleteTemplate (TanStack Query, invalidates ["templates"])
- TemplatesListPage: /admin/templates — table (name, MITRE count, created by, updated),
  New/Edit/Delete actions, loading/error/empty states
- TemplateFormPage: /admin/templates/new + /admin/templates/:id/edit — controlled form
  with inline MITRE field (picker + matrix modal), ConfirmDialog for delete
- TemplatePickerModal: reusable modal listing templates with empty state (AC-27.6)
- SimulationList: replace "New simulation" link with split-button dropdown
  (Blank → /simulations/new | From template… → TemplatePickerModal + POST template_id)
- Layout: "Templates" nav link (admin | redteam, before "Users")
- App.tsx: /admin/templates routes gated roles=["admin","redteam"]
- 26 new Vitest tests (118 total, 92 original preserved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:36:10 +02:00

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();
});
});
});