import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import MockAdapter from 'axios-mock-adapter'; import { apiClient } from '@/api/client'; import { SimulationList } from '@/components/SimulationList'; import { renderWithProviders } from './utils'; import type { Simulation } from '@/api/types'; const SIMULATIONS: Simulation[] = [ { id: 1, engagement_id: 42, name: 'Lateral movement test', techniques: [{ id: 'T1021', name: 'Remote Services', tactics: ['lateral-movement'] }], tactics: [], description: null, commands: null, prerequisites: null, executed_at: '2026-06-01T10:00:00', execution_result: null, log_source: null, logs: null, soc_comment: null, incident_number: null, status: 'in_progress', created_at: '2026-05-26T08:00:00', updated_at: null, created_by: { id: 1, username: 'alice' }, }, ]; let mockCanEdit = true; vi.mock('@/hooks/useAuth', () => ({ useAuth: () => ({ user: { id: 1, username: 'alice', role: mockCanEdit ? 'admin' : 'soc', created_at: '2026-01-01' }, status: 'authenticated', login: vi.fn(), logout: vi.fn(), isAdmin: mockCanEdit, isRedteam: false, isSoc: !mockCanEdit, canEditEngagements: mockCanEdit, }), })); describe('SimulationList — admin/redteam', () => { let mock: MockAdapter; beforeEach(() => { mockCanEdit = true; mock = new MockAdapter(apiClient); }); afterEach(() => { mock.restore(); }); it('shows loading state initially', () => { mock.onGet('/engagements/42/simulations').reply(() => new Promise(() => {})); renderWithProviders(); expect(screen.getByTestId('loading-state')).toBeInTheDocument(); }); it('shows error state when request fails', async () => { mock.onGet('/engagements/42/simulations').reply(500, { error: 'Server error' }); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('error-state')).toBeInTheDocument(); }); }); it('shows empty state when no simulations', async () => { mock.onGet('/engagements/42/simulations').reply(200, []); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('empty-state')).toBeInTheDocument(); }); }); it('shows "Nouvelle simulation" button for admin/redteam in empty state', async () => { mock.onGet('/engagements/42/simulations').reply(200, []); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('new-simulation-btn')).toBeInTheDocument(); }); }); it('renders the simulation list with correct data', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); expect(screen.getByText('T1021')).toBeInTheDocument(); expect(screen.getByTestId('simulation-status-badge')).toHaveAttribute('data-status', 'in_progress'); }); it('shows "Nouvelle simulation" button in header when simulations exist', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); expect(screen.getByTestId('new-simulation-btn')).toBeInTheDocument(); }); it('shows dropdown toggle button when simulations exist', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); expect(screen.getByTestId('new-simulation-dropdown-toggle')).toBeInTheDocument(); }); it('opens dropdown and shows "From template…" option', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); const user = userEvent.setup(); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); await user.click(screen.getByTestId('new-simulation-dropdown-toggle')); expect(screen.getByTestId('from-template-btn')).toBeInTheDocument(); expect(screen.getByText('Blank')).toBeInTheDocument(); }); it('opens TemplatePickerModal when "From template…" is clicked', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); mock.onGet('/templates').reply(200, []); const user = userEvent.setup(); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); await user.click(screen.getByTestId('new-simulation-dropdown-toggle')); await user.click(screen.getByTestId('from-template-btn')); await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument(); }); }); it('clicking a row uses SPA navigation and does not trigger window.location change', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); const originalHref = window.location.href; renderWithProviders(, { routerProps: { initialEntries: ['/engagements/42'] }, }); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); const row = screen.getByText('Lateral movement test').closest('tr') as HTMLElement; fireEvent.click(row); // window.location.href must be unchanged (no full-page reload) expect(window.location.href).toBe(originalHref); }); }); describe('SimulationList — SOC role (no edit button)', () => { let mock: MockAdapter; beforeEach(() => { mockCanEdit = false; mock = new MockAdapter(apiClient); }); afterEach(() => { mock.restore(); }); it('does not show "Nouvelle simulation" button for SOC in empty state', async () => { mock.onGet('/engagements/42/simulations').reply(200, []); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('empty-state')).toBeInTheDocument(); }); expect(screen.queryByTestId('new-simulation-btn')).toBeNull(); }); it('does not show "Nouvelle simulation" button for SOC when simulations exist', async () => { mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Lateral movement test')).toBeInTheDocument(); }); expect(screen.queryByTestId('new-simulation-btn')).toBeNull(); }); });