import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { screen, waitFor, fireEvent } from '@testing-library/react'; // Reset hash between tests so useHashTab doesn't bleed state afterEach(() => { history.replaceState(null, '', '/'); }); import { Route, Routes } from 'react-router-dom'; import MockAdapter from 'axios-mock-adapter'; import { apiClient } from '@/api/client'; import { SimulationFormPage } from '@/pages/SimulationFormPage'; import { renderWithProviders } from './utils'; import type { Simulation } from '@/api/types'; const BASE_SIM: Simulation = { id: 7, engagement_id: 42, name: 'Recon test', techniques: [], tactics: [], description: 'Some description', commands: 'whoami\nipconfig', prerequisites: null, executed_at: null, execution_result: null, log_source: null, logs: null, soc_comment: null, incident_number: null, status: 'pending', created_at: '2026-05-26T08:00:00', updated_at: null, created_by: { id: 1, username: 'alice' }, }; let mockRole: 'admin' | 'redteam' | 'soc' = 'redteam'; vi.mock('@/hooks/useAuth', () => ({ useAuth: () => ({ user: { id: 1, username: 'alice', role: mockRole, created_at: '2026-01-01' }, status: 'authenticated', login: vi.fn(), logout: vi.fn(), isAdmin: mockRole === 'admin', isRedteam: mockRole === 'redteam', isSoc: mockRole === 'soc', canEditEngagements: mockRole === 'admin' || mockRole === 'redteam', }), })); function EditPage() { return ( } /> ); } function NewPage() { return ( } /> ); } describe('SimulationFormPage — redteam mode (edit existing)', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'redteam'; mock = new MockAdapter(apiClient); mock.onGet('/simulations/7').reply(200, BASE_SIM); mock.onGet('/engagements/42/c2-config').reply(404); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); }); afterEach(() => { mock.restore(); }); it('renders loading state initially', () => { mock.onGet('/simulations/7').reply(() => new Promise(() => {})); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); expect(screen.getByTestId('loading-state')).toBeInTheDocument(); }); it('all Red Team fields are enabled for redteam', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled(); }); expect(screen.getByLabelText(/^Description/i)).not.toBeDisabled(); expect(screen.getByLabelText(/^Commandes/i)).not.toBeDisabled(); expect(screen.getByLabelText(/^Exécuté le/i)).not.toBeDisabled(); }); it('shows "Mark for review" button when status is pending', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByTestId('mark-review-btn')).toBeInTheDocument(); }); }); it('does not show "Close" when status is pending', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => screen.getByTestId('mark-review-btn')); expect(screen.queryByTestId('close-btn')).toBeNull(); }); it('shows "Mark for review" for in_progress status', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'in_progress' }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByTestId('mark-review-btn')).toBeInTheDocument(); }); }); it('shows "Close" button on SOC tab when status is review_required', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'review_required' }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); // Switch to SOC tab to see Close button await waitFor(() => expect(screen.getByRole('tab', { name: /SOC/i })).not.toBeDisabled()); fireEvent.click(screen.getByRole('tab', { name: /SOC/i })); await waitFor(() => { expect(screen.getByTestId('close-btn')).toBeInTheDocument(); }); }); it('shows "Delete" button for redteam', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByTestId('delete-btn')).toBeInTheDocument(); }); }); }); describe('SimulationFormPage — tabs', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'redteam'; mock = new MockAdapter(apiClient); mock.onGet('/simulations/7').reply(200, BASE_SIM); mock.onGet('/engagements/42/c2-config').reply(404); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); }); afterEach(() => { mock.restore(); }); it('renders 3 tabs: Red Team, SOC, Tâche C2', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled()); expect(screen.getByRole('tab', { name: /Red Team/i })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /SOC/i })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /Tâche C2/i })).toBeInTheDocument(); }); it('SOC tab is disabled when status is pending', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled()); const socTab = screen.getByRole('tab', { name: /SOC/i }); expect(socTab).toBeDisabled(); }); it('SOC tab is enabled when status is review_required', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'review_required' }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { const socTab = screen.getByRole('tab', { name: /SOC/i }); expect(socTab).not.toBeDisabled(); }); }); it('SOC tab is enabled when status is done', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'done' }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { const socTab = screen.getByRole('tab', { name: /SOC/i }); expect(socTab).not.toBeDisabled(); }); }); it('C2 tab is disabled in edit mode (no tasks shown until tab visible)', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled()); // C2 tab should exist and be enabled (it's edit mode, not create mode) const c2Tab = screen.getByRole('tab', { name: /Tâche C2/i }); expect(c2Tab).not.toBeDisabled(); }); it('SOC fields are visible after switching to SOC tab', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'review_required' }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByRole('tab', { name: /SOC/i })).not.toBeDisabled()); fireEvent.click(screen.getByRole('tab', { name: /SOC/i })); await waitFor(() => { expect(screen.getByLabelText(/^Source de log/i)).toBeInTheDocument(); }); expect(screen.getByLabelText(/^Numéro d'incident/i)).toBeInTheDocument(); }); it('sticky bar is not rendered on C2 tab', async () => { mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled()); fireEvent.click(screen.getByRole('tab', { name: /Tâche C2/i })); await waitFor(() => expect(screen.getByTestId('c2-tasks-panel')).toBeInTheDocument()); // Save button should not appear on C2 tab expect(screen.queryByRole('button', { name: /Enregistrer/i })).toBeNull(); }); it('C2 tasks count pill shows when tasks exist', async () => { mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [ { id: 1, mythic_task_display_id: 10, callback_display_id: 1, command: 'whoami', params: null, status: 'completed', completed: true, output: 'SYSTEM', mapping_applied: false, source: 'import', created_at: '2026-06-10T10:00:00', completed_at: '2026-06-10T10:00:05', }, ], }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { // Count pill shows "1" on the C2 tab expect(screen.getByText('1')).toBeInTheDocument(); }); }); }); describe('SimulationFormPage — SOC role + pending (tab disabled)', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'soc'; mock = new MockAdapter(apiClient); mock.onGet('/simulations/7').reply(200, BASE_SIM); mock.onGet('/engagements/42/c2-config').reply(404); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); }); afterEach(() => { mock.restore(); }); it('SOC tab is disabled for SOC role when status is pending', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => expect(screen.getByRole('tab', { name: /SOC/i })).toBeDisabled()); }); it('Red Team inputs are disabled for SOC', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByLabelText(/^Nom/i)).toBeDisabled(); }); expect(screen.getByLabelText(/^Description/i)).toBeDisabled(); }); }); describe('SimulationFormPage — SOC role + review_required (can edit SOC fields)', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'soc'; mock = new MockAdapter(apiClient); mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'review_required' }); mock.onGet('/engagements/42/c2-config').reply(404); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); }); afterEach(() => { mock.restore(); }); it('SOC inputs are enabled when status is review_required', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); // Switch to SOC tab await waitFor(() => expect(screen.getByRole('tab', { name: /SOC/i })).not.toBeDisabled()); fireEvent.click(screen.getByRole('tab', { name: /SOC/i })); await waitFor(() => { expect(screen.getByLabelText(/^Source de log/i)).not.toBeDisabled(); }); expect(screen.getByLabelText(/^Numéro d'incident/i)).not.toBeDisabled(); }); it('Red Team inputs remain disabled for SOC even when review_required', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByLabelText(/^Nom/i)).toBeDisabled(); }); }); it('shows "Close" for SOC when review_required', async () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); // Switch to SOC tab await waitFor(() => expect(screen.getByRole('tab', { name: /SOC/i })).not.toBeDisabled()); fireEvent.click(screen.getByRole('tab', { name: /SOC/i })); await waitFor(() => { expect(screen.getByTestId('close-btn')).toBeInTheDocument(); }); }); }); describe('SimulationFormPage — new simulation', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'redteam'; mock = new MockAdapter(apiClient); }); afterEach(() => { mock.restore(); }); it('renders the new simulation form with name field', () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/new'] }, }); expect(screen.getByLabelText(/^Nom/i)).toBeInTheDocument(); expect(screen.getByTestId('create-sim-btn')).toBeInTheDocument(); }); it('SOC and C2 tabs are disabled in create mode', () => { renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/new'] }, }); expect(screen.getByRole('tab', { name: /SOC/i })).toBeDisabled(); expect(screen.getByRole('tab', { name: /Tâche C2/i })).toBeDisabled(); }); }); describe('SimulationFormPage — Execute via C2 button visibility', () => { let mock: MockAdapter; beforeEach(() => { mockRole = 'redteam'; mock = new MockAdapter(apiClient); mock.onGet('/simulations/7').reply(200, BASE_SIM); }); afterEach(() => { mock.restore(); }); it('shows Execute via C2 button when c2 config exists', async () => { mock.onGet('/engagements/42/c2-config').reply(200, { has_token: true, url: 'https://mythic.lab:7443', verify_tls: true, }); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByTestId('c2-execute-btn')).toBeInTheDocument(); }); }); it('hides Execute via C2 button when no c2 config (404)', async () => { mock.onGet('/engagements/42/c2-config').reply(404); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled(); }); expect(screen.queryByTestId('c2-execute-btn')).toBeNull(); }); it('hides Execute via C2 button when simulation is done', async () => { mock.onGet('/simulations/7').reply(200, { ...BASE_SIM, status: 'done' }); mock.onGet('/engagements/42/c2-config').reply(200, { has_token: true, url: 'https://mythic.lab:7443', verify_tls: true, }); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); // Wait for RT tab to load; done sim disables RT editing await waitFor(() => expect(screen.getByLabelText(/^Nom/i)).toBeDisabled()); // Execute via C2 not shown on done simulation expect(screen.queryByTestId('c2-execute-btn')).toBeNull(); // Reopen is on the SOC tab — switch to confirm const socTab = screen.getByRole('tab', { name: /SOC/i }); expect(socTab).not.toBeDisabled(); fireEvent.click(socTab); await waitFor(() => { expect(screen.getByTestId('reopen-btn')).toBeInTheDocument(); }); }); it('shows Import C2 history button when c2 config exists', async () => { mock.onGet('/engagements/42/c2-config').reply(200, { has_token: true, url: 'https://mythic.lab:7443', verify_tls: true, }); mock.onGet('/simulations/7/c2/tasks').reply(200, { tasks: [] }); renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] }, }); await waitFor(() => { expect(screen.getByTestId('c2-import-trigger-btn')).toBeInTheDocument(); }); }); });