feat: sprint 5 — simulation templates + instantiation + nav + dropdown #8
@@ -6,19 +6,19 @@ import type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export async function listTemplates(): Promise<SimulationTemplate[]> {
|
export async function listTemplates(): Promise<SimulationTemplate[]> {
|
||||||
const { data } = await apiClient.get<SimulationTemplate[]>('/simulation-templates');
|
const { data } = await apiClient.get<SimulationTemplate[]>('/templates');
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTemplate(id: number): Promise<SimulationTemplate> {
|
export async function getTemplate(id: number): Promise<SimulationTemplate> {
|
||||||
const { data } = await apiClient.get<SimulationTemplate>(`/simulation-templates/${id}`);
|
const { data } = await apiClient.get<SimulationTemplate>(`/templates/${id}`);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTemplate(
|
export async function createTemplate(
|
||||||
input: SimulationTemplateCreateInput,
|
input: SimulationTemplateCreateInput,
|
||||||
): Promise<SimulationTemplate> {
|
): Promise<SimulationTemplate> {
|
||||||
const { data } = await apiClient.post<SimulationTemplate>('/simulation-templates', input);
|
const { data } = await apiClient.post<SimulationTemplate>('/templates', input);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,10 +26,10 @@ export async function updateTemplate(
|
|||||||
id: number,
|
id: number,
|
||||||
patch: SimulationTemplatePatchInput,
|
patch: SimulationTemplatePatchInput,
|
||||||
): Promise<SimulationTemplate> {
|
): Promise<SimulationTemplate> {
|
||||||
const { data } = await apiClient.patch<SimulationTemplate>(`/simulation-templates/${id}`, patch);
|
const { data } = await apiClient.patch<SimulationTemplate>(`/templates/${id}`, patch);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTemplate(id: number): Promise<void> {
|
export async function deleteTemplate(id: number): Promise<void> {
|
||||||
await apiClient.delete(`/simulation-templates/${id}`);
|
await apiClient.delete(`/templates/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ describe('SimulationList — admin/redteam', () => {
|
|||||||
|
|
||||||
it('opens TemplatePickerModal when "From template…" is clicked', async () => {
|
it('opens TemplatePickerModal when "From template…" is clicked', async () => {
|
||||||
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
||||||
mock.onGet('/simulation-templates').reply(200, []);
|
mock.onGet('/templates').reply(200, []);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderWithProviders(<SimulationList engagementId={42} />);
|
renderWithProviders(<SimulationList engagementId={42} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ describe('TemplateFormPage — new mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('submits POST when name is filled', async () => {
|
it('submits POST when name is filled', async () => {
|
||||||
mock.onPost('/simulation-templates').reply(201, { ...TEMPLATE, id: 99 });
|
mock.onPost('/templates').reply(201, { ...TEMPLATE, id: 99 });
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderNew();
|
renderNew();
|
||||||
await user.type(screen.getByLabelText(/Name/i), 'My Template');
|
await user.type(screen.getByLabelText(/Name/i), 'My Template');
|
||||||
@@ -125,7 +125,7 @@ describe('TemplateFormPage — new mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows backend error on name conflict (409)', async () => {
|
it('shows backend error on name conflict (409)', async () => {
|
||||||
mock.onPost('/simulation-templates').reply(409, { error: 'template name already exists' });
|
mock.onPost('/templates').reply(409, { error: 'template name already exists' });
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderNew();
|
renderNew();
|
||||||
await user.type(screen.getByLabelText(/Name/i), 'Duplicate');
|
await user.type(screen.getByLabelText(/Name/i), 'Duplicate');
|
||||||
@@ -153,7 +153,7 @@ describe('TemplateFormPage — edit mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads existing template data into form', async () => {
|
it('loads existing template data into form', async () => {
|
||||||
mock.onGet('/simulation-templates/5').reply(200, TEMPLATE);
|
mock.onGet('/templates/5').reply(200, TEMPLATE);
|
||||||
renderEdit(5);
|
renderEdit(5);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByDisplayValue('Mimikatz LSASS Dump')).toBeInTheDocument();
|
expect(screen.getByDisplayValue('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||||||
@@ -162,7 +162,7 @@ describe('TemplateFormPage — edit mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows technique and tactic chips from existing template', async () => {
|
it('shows technique and tactic chips from existing template', async () => {
|
||||||
mock.onGet('/simulation-templates/5').reply(200, TEMPLATE);
|
mock.onGet('/templates/5').reply(200, TEMPLATE);
|
||||||
renderEdit(5);
|
renderEdit(5);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle('T1003 — OS Credential Dumping')).toBeInTheDocument();
|
expect(screen.getByTitle('T1003 — OS Credential Dumping')).toBeInTheDocument();
|
||||||
@@ -171,7 +171,7 @@ describe('TemplateFormPage — edit mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows Delete button in edit mode', async () => {
|
it('shows Delete button in edit mode', async () => {
|
||||||
mock.onGet('/simulation-templates/5').reply(200, TEMPLATE);
|
mock.onGet('/templates/5').reply(200, TEMPLATE);
|
||||||
renderEdit(5);
|
renderEdit(5);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByDisplayValue('Mimikatz LSASS Dump')).toBeInTheDocument();
|
expect(screen.getByDisplayValue('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||||||
@@ -180,8 +180,8 @@ describe('TemplateFormPage — edit mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('submits PATCH on save', async () => {
|
it('submits PATCH on save', async () => {
|
||||||
mock.onGet('/simulation-templates/5').reply(200, TEMPLATE);
|
mock.onGet('/templates/5').reply(200, TEMPLATE);
|
||||||
mock.onPatch('/simulation-templates/5').reply(200, { ...TEMPLATE, name: 'Updated' });
|
mock.onPatch('/templates/5').reply(200, { ...TEMPLATE, name: 'Updated' });
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderEdit(5);
|
renderEdit(5);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -196,8 +196,8 @@ describe('TemplateFormPage — edit mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('opens delete confirm dialog and calls DELETE on confirm', async () => {
|
it('opens delete confirm dialog and calls DELETE on confirm', async () => {
|
||||||
mock.onGet('/simulation-templates/5').reply(200, TEMPLATE);
|
mock.onGet('/templates/5').reply(200, TEMPLATE);
|
||||||
mock.onDelete('/simulation-templates/5').reply(204);
|
mock.onDelete('/templates/5').reply(204);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderEdit(5);
|
renderEdit(5);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows loading state while fetching', () => {
|
it('shows loading state while fetching', () => {
|
||||||
mock.onGet('/simulation-templates').reply(() => new Promise(() => {}));
|
mock.onGet('/templates').reply(() => new Promise(() => {}));
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
engagementId={1}
|
engagementId={1}
|
||||||
@@ -64,7 +64,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state when no templates', async () => {
|
it('shows empty state when no templates', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, []);
|
mock.onGet('/templates').reply(200, []);
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
engagementId={1}
|
engagementId={1}
|
||||||
@@ -80,7 +80,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('lists templates with name and MITRE count', async () => {
|
it('lists templates with name and MITRE count', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
engagementId={1}
|
engagementId={1}
|
||||||
@@ -99,7 +99,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls onSelectTemplate when a template row is clicked', async () => {
|
it('calls onSelectTemplate when a template row is clicked', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
@@ -117,7 +117,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls onClose when Cancel is clicked', async () => {
|
it('calls onClose when Cancel is clicked', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
@@ -135,7 +135,7 @@ describe('TemplatePickerModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows error state on fetch failure', async () => {
|
it('shows error state on fetch failure', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(500, { error: 'Server error' });
|
mock.onGet('/templates').reply(500, { error: 'Server error' });
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<TemplatePickerModal
|
<TemplatePickerModal
|
||||||
engagementId={1}
|
engagementId={1}
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows loading state initially', () => {
|
it('shows loading state initially', () => {
|
||||||
mock.onGet('/simulation-templates').reply(() => new Promise(() => {}));
|
mock.onGet('/templates').reply(() => new Promise(() => {}));
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
expect(screen.getByTestId('loading-state')).toBeInTheDocument();
|
expect(screen.getByTestId('loading-state')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows error state on failure', async () => {
|
it('shows error state on failure', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(500, { error: 'Server error' });
|
mock.onGet('/templates').reply(500, { error: 'Server error' });
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('error-state')).toBeInTheDocument();
|
expect(screen.getByTestId('error-state')).toBeInTheDocument();
|
||||||
@@ -73,7 +73,7 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state when no templates', async () => {
|
it('shows empty state when no templates', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, []);
|
mock.onGet('/templates').reply(200, []);
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
||||||
@@ -81,7 +81,7 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders template list with name, MITRE count, created by', async () => {
|
it('renders template list with name, MITRE count, created by', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||||||
@@ -95,7 +95,7 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows New button', async () => {
|
it('shows New button', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||||||
@@ -104,7 +104,7 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows Edit and Delete actions', async () => {
|
it('shows Edit and Delete actions', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
renderWithProviders(<TemplatesListPage />);
|
renderWithProviders(<TemplatesListPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument();
|
||||||
@@ -114,10 +114,10 @@ describe('TemplatesListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls delete endpoint on confirm', async () => {
|
it('calls delete endpoint on confirm', async () => {
|
||||||
mock.onGet('/simulation-templates').reply(200, TEMPLATES);
|
mock.onGet('/templates').reply(200, TEMPLATES);
|
||||||
mock.onDelete('/simulation-templates/1').reply(204);
|
mock.onDelete('/templates/1').reply(204);
|
||||||
// After delete, refetch returns updated list
|
// After delete, refetch returns updated list
|
||||||
mock.onGet('/simulation-templates').reply(200, [TEMPLATES[1]]);
|
mock.onGet('/templates').reply(200, [TEMPLATES[1]]);
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user