Files
mimic/frontend/tests/MitreTechniquesField.test.tsx
2026-06-21 23:41:32 +02:00

168 lines
6.3 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 { 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 { MitreTechniquesField } from '@/components/MitreTechniquesField';
import { renderWithProviders } from './utils';
import type { MitreTechnique, MitreTacticRef } from '@/api/types';
const T1059: MitreTechnique = { id: 'T1059', name: 'Command and Scripting Interpreter', tactics: ['execution'] };
const T1078: MitreTechnique = { id: 'T1078', name: 'Valid Accounts', tactics: ['initial-access'] };
const TA0007: MitreTacticRef = { id: 'TA0007', name: 'Discovery' };
vi.mock('@/hooks/useAuth', () => ({
useAuth: () => ({
user: { id: 1, username: 'alice', role: 'redteam', created_at: '2026-01-01' },
status: 'authenticated',
login: vi.fn(),
logout: vi.fn(),
isAdmin: false,
isRedteam: true,
isSoc: false,
canEditEngagements: true,
}),
}));
const SIM_RESPONSE = {
id: 7, engagement_id: 42, name: 'test', techniques: [], tactics: [],
description: null, commands: null, prerequisites: null,
executed_at: null, execution_result: null, log_source: null,
logs: null, soc_comment: null, incident_number: null,
status: 'pending', created_at: '2026-01-01', updated_at: null,
created_by: { id: 1, username: 'alice' },
};
describe('MitreTechniquesField', () => {
let mock: MockAdapter;
beforeEach(() => {
mock = new MockAdapter(apiClient);
});
afterEach(() => {
mock.restore();
});
it('shows empty state message when no techniques or tactics', () => {
renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
);
expect(screen.getByText(/Aucune technique associée/i)).toBeInTheDocument();
});
it('renders technique tags for each technique', () => {
renderWithProviders(
<MitreTechniquesField value={[T1059, T1078]} tactics={[]} simulationId={7} engagementId={42} />,
);
expect(screen.getAllByTestId('mitre-technique-tag')).toHaveLength(2);
expect(screen.getByTitle(/T1059/)).toBeInTheDocument();
expect(screen.getByTitle(/T1078/)).toBeInTheDocument();
});
it('renders tactic chips alongside technique chips', () => {
renderWithProviders(
<MitreTechniquesField value={[T1059]} tactics={[TA0007]} simulationId={7} engagementId={42} />,
);
expect(screen.getAllByTestId('mitre-tactic-tag')).toHaveLength(1);
expect(screen.getByTitle(/TA0007/)).toBeInTheDocument();
});
it('shows search input and matrix icon when not disabled', () => {
renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
);
expect(screen.getByRole('button', { name: /Ouvrir la matrice/i })).toBeInTheDocument();
// The search placeholder button
expect(screen.getByRole('button', { name: /Rechercher une technique/i })).toBeInTheDocument();
});
it('hides input row when disabled', () => {
renderWithProviders(
<MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} disabled />,
);
expect(screen.queryByRole('button', { name: /Ouvrir la matrice/i })).toBeNull();
});
it('× button on technique tag calls PATCH with technique removed', async () => {
mock.onPatch('/simulations/7').reply(200, SIM_RESPONSE);
mock.onGet('/engagements/42/simulations').reply(200, []);
mock.onGet('/simulations/7').reply(200, SIM_RESPONSE);
const user = userEvent.setup();
renderWithProviders(
<MitreTechniquesField value={[T1059, T1078]} tactics={[]} simulationId={7} engagementId={42} />,
);
const removeBtn = screen.getByRole('button', { name: /Retirer T1059/i });
await user.click(removeBtn);
await waitFor(() => {
expect(mock.history.patch.length).toBe(1);
const body = JSON.parse(mock.history.patch[0].data as string);
expect(body.technique_ids).toEqual(['T1078']);
expect(body.tactic_ids).toEqual([]);
});
});
it('× button on tactic tag calls PATCH with tactic removed', async () => {
mock.onPatch('/simulations/7').reply(200, SIM_RESPONSE);
mock.onGet('/engagements/42/simulations').reply(200, []);
mock.onGet('/simulations/7').reply(200, SIM_RESPONSE);
const user = userEvent.setup();
renderWithProviders(
<MitreTechniquesField value={[T1059]} tactics={[TA0007]} simulationId={7} engagementId={42} />,
);
const removeBtn = screen.getByRole('button', { name: /Retirer TA0007/i });
await user.click(removeBtn);
await waitFor(() => {
expect(mock.history.patch.length).toBe(1);
const body = JSON.parse(mock.history.patch[0].data as string);
expect(body.tactic_ids).toEqual([]);
expect(body.technique_ids).toEqual(['T1059']);
});
});
it('clicking search placeholder shows combobox input', async () => {
const user = userEvent.setup();
renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
);
await user.click(screen.getByRole('button', { name: /Rechercher une technique/i }));
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('dedup: selecting an already-present technique does not PATCH', async () => {
mock.onGet('/mitre/techniques').reply(200, [T1059]);
const user = userEvent.setup();
renderWithProviders(
<MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} />,
);
await user.click(screen.getByRole('button', { name: /Rechercher une technique/i }));
const combobox = screen.getByRole('combobox');
await user.type(combobox, 'T1059');
const option = await screen.findByRole('option', { name: /T1059/i });
expect(option).toBeInTheDocument();
await user.pointer({ target: option, keys: '[MouseLeft>]' });
expect(mock.history.patch.length).toBe(0);
});
it('opens matrix modal when matrix icon is clicked', async () => {
mock.onGet('/mitre/matrix').reply(200, []);
const user = userEvent.setup();
renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
);
await user.click(screen.getByRole('button', { name: /Ouvrir la matrice/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
});