Files
mimic/e2e/tests/us22-mitre-input-redesign.spec.ts

251 lines
9.2 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<Simulation> {
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<void> {
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);
});
});