import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { screen, waitFor, fireEvent } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { apiClient } from '@/api/client'; import { ImportC2HistoryModal } from '@/components/ImportC2HistoryModal'; import { ToastViewport } from '@/components/Toast'; import { renderWithProviders } from '../utils'; 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 CALLBACKS = [ { display_id: 1, active: true, host: 'WIN-TARGET', user: 'administrator', domain: 'lab.local', last_checkin: '2026-06-10T10:00:00', }, { display_id: 2, active: false, host: 'WIN-DC01', user: 'SYSTEM', domain: 'lab.local', last_checkin: '2026-06-10T09:00:00', }, ]; const HISTORY_TASKS = [ { display_id: 10, command: 'whoami', status: 'completed', completed: true, completed_at: '2026-06-10T10:00:05', created_at: '2026-06-10T10:00:00', }, { display_id: 11, command: 'ipconfig', status: 'completed', completed: true, completed_at: '2026-06-10T10:00:10', created_at: '2026-06-10T10:00:05', }, ]; let mock: MockAdapter; beforeEach(() => { mock = new MockAdapter(apiClient); mock.onGet('/engagements/42/c2/callbacks').reply(200, { callbacks: CALLBACKS }); }); afterEach(() => { mock.restore(); vi.clearAllMocks(); }); function renderModal() { const onClose = vi.fn(); renderWithProviders( <> , ); return { onClose }; } describe('ImportC2HistoryModal — step 1: callback picker', () => { it('renders modal with title and callback rows', async () => { renderModal(); expect(screen.getByTestId('c2-import-modal')).toBeInTheDocument(); expect(screen.getByText('Import C2 history')).toBeInTheDocument(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); }); it('history table is not shown before selecting a callback', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); expect(screen.queryByTestId('c2-history-row')).toBeNull(); }); it('Import button is disabled with no selection', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); expect(screen.getByTestId('c2-import-submit-btn')).toBeDisabled(); }); }); describe('ImportC2HistoryModal — step 2: history table appears after callback select', () => { beforeEach(() => { mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: HISTORY_TASKS, total: 2, page: 1, page_size: 25, }); }); it('shows history table after selecting a callback', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(2); }); }); it('shows history task commands in the table', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getByText('whoami')).toBeInTheDocument(); expect(screen.getByText('ipconfig')).toBeInTheDocument(); }); }); }); describe('ImportC2HistoryModal — multi-checkbox selection', () => { beforeEach(() => { mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: HISTORY_TASKS, total: 2, page: 1, page_size: 25, }); }); it('Import button remains disabled with no tasks checked', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(2); }); expect(screen.getByTestId('c2-import-submit-btn')).toBeDisabled(); }); it('Import button becomes enabled after checking a task row', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); expect(screen.getByTestId('c2-import-submit-btn')).not.toBeDisabled(); }); it('checking via checkbox also enables Import', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row-checkbox')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-history-row-checkbox')[1]); expect(screen.getByTestId('c2-import-submit-btn')).not.toBeDisabled(); }); it('unchecking a row disables Import when it was the only selection', async () => { renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(2); }); // Check then uncheck fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); expect(screen.getByTestId('c2-import-submit-btn')).toBeDisabled(); }); }); describe('ImportC2HistoryModal — pagination', () => { it('shows Prev/Next buttons when tasks exceed page_size', async () => { // 30 tasks, page_size 25 → 2 pages const manyTasks = Array.from({ length: 25 }, (_, i) => ({ display_id: i + 1, command: `cmd${i + 1}`, status: 'completed', completed: true, completed_at: '2026-06-10T10:00:00', created_at: '2026-06-10T10:00:00', })); mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: manyTasks, total: 30, page: 1, page_size: 25, }); renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getByTestId('c2-history-prev')).toBeInTheDocument(); expect(screen.getByTestId('c2-history-next')).toBeInTheDocument(); }); }); it('Prev is disabled on page 1', async () => { mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: HISTORY_TASKS, total: 50, page: 1, page_size: 25, }); renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getByTestId('c2-history-prev')).toBeDisabled(); }); expect(screen.getByTestId('c2-history-next')).not.toBeDisabled(); }); }); describe('ImportC2HistoryModal — submit payload', () => { beforeEach(() => { mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: HISTORY_TASKS, total: 2, page: 1, page_size: 25, }); }); it('sends correct callback_display_id and task_display_ids on import', async () => { mock.onPost('/simulations/7/c2/import').reply(200, { imported: 2, skipped: 0 }); const { onClose } = renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(2); }); // Select both tasks fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); fireEvent.click(screen.getAllByTestId('c2-history-row')[1]); fireEvent.click(screen.getByTestId('c2-import-submit-btn')); await waitFor(() => { expect(onClose).toHaveBeenCalled(); }); const req = mock.history['post'][0]; expect(req.url).toBe('/simulations/7/c2/import'); const body = JSON.parse(req.data as string); expect(body.callback_display_id).toBe(1); expect(body.task_display_ids).toContain(10); expect(body.task_display_ids).toContain(11); }); }); describe('ImportC2HistoryModal — toast wording', () => { beforeEach(() => { mock.onGet('/engagements/42/c2/callbacks/1/history').reply(200, { tasks: [HISTORY_TASKS[0]], total: 1, page: 1, page_size: 25, }); }); it('shows "Imported N task(s)" when skipped is 0', async () => { mock.onPost('/simulations/7/c2/import').reply(200, { imported: 1, skipped: 0 }); renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(1); }); fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); fireEvent.click(screen.getByTestId('c2-import-submit-btn')); await waitFor(() => { expect(screen.getByText('Imported 1 task(s)')).toBeInTheDocument(); }); }); it('shows skipped count when skipped > 0', async () => { mock.onPost('/simulations/7/c2/import').reply(200, { imported: 0, skipped: 1 }); renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(1); }); fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); fireEvent.click(screen.getByTestId('c2-import-submit-btn')); await waitFor(() => { expect( screen.getByText('Imported 0 task(s), 1 already attached'), ).toBeInTheDocument(); }); }); it('shows inline error and keeps modal open on import failure', async () => { mock.onPost('/simulations/7/c2/import').reply(500, { error: 'Import failed' }); const { onClose } = renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getAllByTestId('c2-import-callback-row')[0]); await waitFor(() => { expect(screen.getAllByTestId('c2-history-row')).toHaveLength(1); }); fireEvent.click(screen.getAllByTestId('c2-history-row')[0]); fireEvent.click(screen.getByTestId('c2-import-submit-btn')); await waitFor(() => { expect(screen.getByText('Import failed')).toBeInTheDocument(); }); expect(onClose).not.toHaveBeenCalled(); }); }); describe('ImportC2HistoryModal — Cancel button', () => { it('Cancel button calls onClose', async () => { const { onClose } = renderModal(); await waitFor(() => { expect(screen.getAllByTestId('c2-import-callback-row')).toHaveLength(2); }); fireEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(onClose).toHaveBeenCalled(); }); });