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 { TemplatesListPage } from '@/pages/TemplatesListPage'; import { renderWithProviders } from './utils'; import type { SimulationTemplate } from '@/api/types'; const TEMPLATES: SimulationTemplate[] = [ { id: 1, name: 'Mimikatz LSASS Dump', description: 'Extract NTLM hashes', commands: 'mimikatz.exe', prerequisites: 'Local admin', techniques: [{ id: 'T1003', name: 'OS Credential Dumping', tactics: ['credential-access'] }], tactics: [{ id: 'TA0006', name: 'Credential Access' }], created_at: '2026-05-28T00:00:00', updated_at: null, created_by: { id: 1, username: 'alice' }, }, { id: 2, name: 'PowerShell Empire', description: null, commands: null, prerequisites: null, techniques: [], tactics: [], created_at: '2026-05-28T01:00:00', updated_at: '2026-05-28T02:00:00', created_by: { id: 2, username: 'bob' }, }, ]; vi.mock('@/hooks/useAuth', () => ({ useAuth: () => ({ user: { id: 1, username: 'alice', role: 'admin', created_at: '2026-01-01' }, status: 'authenticated', login: vi.fn(), logout: vi.fn(), isAdmin: true, isRedteam: false, isSoc: false, canEditEngagements: true, }), })); describe('TemplatesListPage', () => { let mock: MockAdapter; beforeEach(() => { mock = new MockAdapter(apiClient); }); afterEach(() => { mock.restore(); }); it('shows loading state initially', () => { mock.onGet('/simulation-templates').reply(() => new Promise(() => {})); renderWithProviders(); expect(screen.getByTestId('loading-state')).toBeInTheDocument(); }); it('shows error state on failure', async () => { mock.onGet('/simulation-templates').reply(500, { error: 'Server error' }); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('error-state')).toBeInTheDocument(); }); }); it('shows empty state when no templates', async () => { mock.onGet('/simulation-templates').reply(200, []); renderWithProviders(); await waitFor(() => { expect(screen.getByTestId('empty-state')).toBeInTheDocument(); }); }); it('renders template list with name, MITRE count, created by', async () => { mock.onGet('/simulation-templates').reply(200, TEMPLATES); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument(); }); expect(screen.getByText('PowerShell Empire')).toBeInTheDocument(); // MITRE count: techniques(1) + tactics(1) = 2 for first template expect(screen.getByText('2')).toBeInTheDocument(); // Second template: 0 — shown as — expect(screen.getAllByText('—').length).toBeGreaterThan(0); expect(screen.getByText('alice')).toBeInTheDocument(); }); it('shows New button', async () => { mock.onGet('/simulation-templates').reply(200, TEMPLATES); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument(); }); expect(screen.getAllByText(/New/i).length).toBeGreaterThan(0); }); it('shows Edit and Delete actions', async () => { mock.onGet('/simulation-templates').reply(200, TEMPLATES); renderWithProviders(); await waitFor(() => { expect(screen.getByText('Mimikatz LSASS Dump')).toBeInTheDocument(); }); expect(screen.getAllByText('Edit').length).toBe(2); expect(screen.getAllByText('Delete').length).toBe(2); }); it('calls delete endpoint on confirm', async () => { mock.onGet('/simulation-templates').reply(200, TEMPLATES); mock.onDelete('/simulation-templates/1').reply(204); // After delete, refetch returns updated list mock.onGet('/simulation-templates').reply(200, [TEMPLATES[1]]); const user = userEvent.setup(); const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); renderWithProviders(); await waitFor(() => { expect(screen.getAllByText('Delete')[0]).toBeInTheDocument(); }); const deleteButtons = screen.getAllByText('Delete'); await user.click(deleteButtons[0]); await waitFor(() => { expect(mock.history.delete.length).toBe(1); }); confirmSpy.mockRestore(); }); });