/** * US-22 — Refonte input MITRE dans le form. * Covers AC-22.1 → AC-22.5. * Key change: no "Add technique" / "Quick search" text buttons. * Instead: inline autocomplete input + grid icon for matrix. * Chips show T-id only (name in title=). */ import { test, expect } from '@playwright/test'; import { adminToken, createEngagement, deleteEngagement, deleteUserByUsername, ensureUser, login, makeClient, } from '../fixtures/api'; import { seedTokenInStorage } from '../fixtures/auth'; const REDTEAM_USER = 'us22-redteam'; const SOC_USER = 'us22-soc'; const PASS = 'us22-pass-strong'; interface Simulation { id: number; [key: string]: unknown; } async function createSimulation(token: string, engagementId: number, name = 'US-22 sim'): Promise { const r = await makeClient(token).post(`/engagements/${engagementId}/simulations`, { name }); if (r.status !== 201) throw new Error(`create sim: ${r.status}`); return r.data as Simulation; } async function deleteSimulation(token: string, simId: number): Promise { await makeClient(token).delete(`/simulations/${simId}`); } test.describe('US-22 — MITRE input redesign', () => { let redteamToken: string; let socToken: string; let engagementId: number; test.beforeAll(async () => { await ensureUser(REDTEAM_USER, PASS, 'redteam'); await ensureUser(SOC_USER, PASS, 'soc'); redteamToken = (await login(REDTEAM_USER, PASS)).token; socToken = (await login(SOC_USER, PASS)).token; const eng = await createEngagement(redteamToken, { name: 'US-22 Engagement', start_date: '2026-01-01', }); engagementId = eng.id; }); test.afterAll(async () => { try { const tok = await adminToken(); await deleteEngagement(tok, engagementId); for (const u of [REDTEAM_USER, SOC_USER]) await deleteUserByUsername(tok, u); } catch { /* noop */ } }); test('AC-22.1 — layout: inline autocomplete input + matrix icon present, NO text buttons', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 layout'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Matrix icon button present await expect(page.getByLabel(/open mitre matrix/i)).toBeVisible(); // Search input placeholder visible (inline autocomplete) await expect(page.getByText(/search technique/i)).toBeVisible(); // No old-style text buttons await expect(page.getByRole('button', { name: /add technique/i })).not.toBeVisible(); await expect(page.getByRole('button', { name: /quick search/i })).not.toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); test('AC-22.1 — inline autocomplete: click input shows combobox, type shows dropdown', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 autocomplete'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Click the fake-input placeholder to reveal the combobox await page.getByText(/search technique/i).click(); const picker = page.getByRole('combobox', { name: /mitre technique/i }); await expect(picker).toBeVisible(); // Type to get results await picker.fill('T1059'); const listbox = page.getByRole('listbox', { name: /mitre techniques/i }); await expect(listbox).toBeVisible({ timeout: 5_000 }); // Select via keyboard await picker.press('ArrowDown'); await picker.press('Enter'); // Tag appears await expect(page.getByTestId('techniques-tag-list')).toContainText('T1059', { timeout: 5_000 }); // Auto-save toast await expect(page.getByText(/techniques updated/i)).toBeVisible({ timeout: 5_000 }); await deleteSimulation(redteamToken, sim.id); }); test('AC-22.1 — matrix icon opens MitreMatrixModal', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 matrix icon'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); await page.getByLabel(/open mitre matrix/i).click(); await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 }); await expect(page.getByRole('dialog')).toContainText(/mitre att&?ck matrix/i); await deleteSimulation(redteamToken, sim.id); }); // AC-22.2 — chips show T-id only, name in title test('AC-22.2 — technique chips display T-id only, name in title= attribute', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.2 chip format'); await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); const chip = page.getByTestId('mitre-technique-tag').first(); await expect(chip).toBeVisible(); // Text content is T-id only const text = await chip.textContent(); expect(text?.trim()).toMatch(/^T1059/); // Must NOT contain the full name inline expect(text).not.toMatch(/Command and Scripting Interpreter/); // Name appears in title attribute const title = await chip.getAttribute('title'); expect(title).toMatch(/T1059/); expect(title).toMatch(/Command and Scripting Interpreter/); await deleteSimulation(redteamToken, sim.id); }); test('AC-22.2 — tactic chips display TA-id only, name in title= attribute', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.2 tactic chip format'); await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); const chip = page.getByTestId('mitre-tactic-tag').first(); await expect(chip).toBeVisible(); const text = await chip.textContent(); expect(text?.trim()).toMatch(/^TA0007/); expect(text).not.toMatch(/Discovery/); const title = await chip.getAttribute('title'); expect(title).toMatch(/TA0007/); expect(title).toMatch(/Discovery/); await deleteSimulation(redteamToken, sim.id); }); // AC-22.4 — empty state test('AC-22.4 — empty state: "No techniques selected" visible, input and matrix icon still shown', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.4 empty'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Empty state message await expect(page.getByText(/no techniques selected/i)).toBeVisible(); // Input and matrix icon still present in non-disabled mode await expect(page.getByLabel(/open mitre matrix/i)).toBeVisible(); await expect(page.getByText(/search technique/i)).toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); // AC-22.5 — read-only mode test('AC-22.5 — SOC on in_progress sim: chips visible (no ×), input + matrix icon hidden', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.5 soc readonly'); // Add a technique and advance to review_required for SOC access await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] }); await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'review_required' }); await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Chip is visible await expect(page.getByTestId('mitre-technique-tag')).toBeVisible(); // No × remove button (read-only for SOC on technique chips) await expect(page.getByRole('button', { name: /remove T1059/i })).not.toBeVisible(); // Matrix icon and input hidden in disabled mode await expect(page.getByLabel(/open mitre matrix/i)).not.toBeVisible(); await expect(page.getByText(/search technique/i)).not.toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); test('AC-22.5 — done sim: all chips read-only, no input', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-22.5 done readonly'); await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] }); // Drive to done await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'review_required' }); await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'done' }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Chip visible await expect(page.getByTestId('mitre-technique-tag')).toBeVisible(); // No × remove await expect(page.getByRole('button', { name: /remove T1059/i })).not.toBeVisible(); // No matrix icon await expect(page.getByLabel(/open mitre matrix/i)).not.toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); });