From 54959c7d5b4d44cc29bc7f5c58aa7a9ed2c09330 Mon Sep 17 00:00:00 2001 From: Knacky Date: Thu, 28 May 2026 07:23:33 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20sprint=205=20acceptance=20?= =?UTF-8?q?=E2=80=94=20US-26=20/=20US-27=20/=20US-28=20+=20adaptations=20d?= =?UTF-8?q?ropdown=20sprint=202-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - us26: add AC-26.4 isinstance guard (technique_ids string→400) + AC-26.7 cascade test (DELETE template does not affect instantiated sim) - us27: add NIT-1 dropdown Escape/click-outside close, NIT-2 empty-engagement dropdown visibility - 49 sprint 5 tests passing, 206/207 full suite passing (us1 pre-existing isolation issue) Co-Authored-By: Claude Sonnet 4.6 --- e2e/tests/us26-templates-crud.spec.ts | 50 ++++++++++++++++ .../us27-instantiate-from-template.spec.ts | 57 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/e2e/tests/us26-templates-crud.spec.ts b/e2e/tests/us26-templates-crud.spec.ts index 32f6f2a..95c7443 100644 --- a/e2e/tests/us26-templates-crud.spec.ts +++ b/e2e/tests/us26-templates-crud.spec.ts @@ -153,6 +153,15 @@ test.describe('US-26 — templates CRUD', () => { expect(r.data.error).toMatch(/unknown tactic id.*TA9999/i); }); + test('AC-26.4 — POST technique_ids as string (not list) → 400 (isinstance guard)', async () => { + const r = await makeClient(redteamToken).post('/templates', { + name: 'US26 bad technique_ids type', + technique_ids: 'T1059', + }); + expect(r.status).toBe(400); + expect(r.data.error).toMatch(/technique_ids must be a list/i); + }); + test('AC-26.4 — SOC POST → 403', async () => { const r = await makeClient(socToken).post('/templates', { name: 'soc template attempt' }); expect(r.status).toBe(403); @@ -239,6 +248,47 @@ test.describe('US-26 — templates CRUD', () => { await deleteTemplate(redteamToken, t.id); }); + test('AC-26.7 — DELETE template does NOT cascade to instantiated simulations', async () => { + const tok = await adminToken(); + // Create engagement + const engR = await makeClient(tok).post('/engagements', { + name: 'US26 cascade eng', + start_date: '2026-01-01', + }); + expect(engR.status).toBe(201); + const engId = engR.data.id as number; + + // Create template with distinct RT fields + const tmpl = await createTemplate(redteamToken, { + name: 'US26 cascade template', + description: 'cascade test desc', + commands: 'cascade cmd', + tactic_ids: ['TA0007'], + }); + + // Instantiate simulation from template + const simR = await makeClient(redteamToken).post(`/engagements/${engId}/simulations`, { + template_id: tmpl.id, + }); + expect(simR.status).toBe(201); + const simId = simR.data.id as number; + + // Delete the template + const del = await makeClient(redteamToken).delete(`/templates/${tmpl.id}`); + expect(del.status).toBe(204); + + // Simulation must still exist with RT fields copied at instantiation time + const simCheck = await makeClient(redteamToken).get(`/simulations/${simId}`); + expect(simCheck.status).toBe(200); + expect(simCheck.data.name).toBe('US26 cascade template'); + expect(simCheck.data.description).toBe('cascade test desc'); + expect(simCheck.data.commands).toBe('cascade cmd'); + + // Cleanup + await makeClient(tok).delete(`/simulations/${simId}`); + await makeClient(tok).delete(`/engagements/${engId}`); + }); + // AC-26.8 — UI /admin/templates page test('AC-26.8 — /admin/templates page is accessible to redteam, shows table + New button', async ({ page, diff --git a/e2e/tests/us27-instantiate-from-template.spec.ts b/e2e/tests/us27-instantiate-from-template.spec.ts index df6c50f..e6d7412 100644 --- a/e2e/tests/us27-instantiate-from-template.spec.ts +++ b/e2e/tests/us27-instantiate-from-template.spec.ts @@ -336,4 +336,61 @@ test.describe('US-27 — instantiate from template', () => { await deleteSimulation(redteamToken, simId); }); + + // NIT 1 — Dropdown closes on Escape key and on outside click + test('NIT-1 — dropdown closes on Escape key press', async ({ + page, + context, + }) => { + await seedTokenInStorage(context, redteamToken); + await page.goto(`/engagements/${engagementId}`); + + await page.getByTestId('new-simulation-dropdown-toggle').click(); + // Menu is open + await expect(page.getByTestId('from-template-btn')).toBeVisible({ timeout: 3_000 }); + + // Press Escape + await page.keyboard.press('Escape'); + await expect(page.getByTestId('from-template-btn')).not.toBeVisible({ timeout: 3_000 }); + }); + + test('NIT-1 — dropdown closes when clicking outside', async ({ + page, + context, + }) => { + await seedTokenInStorage(context, redteamToken); + await page.goto(`/engagements/${engagementId}`); + + await page.getByTestId('new-simulation-dropdown-toggle').click(); + await expect(page.getByTestId('from-template-btn')).toBeVisible({ timeout: 3_000 }); + + // Click somewhere outside the dropdown (page heading) + await page.getByRole('heading').first().click({ force: true }); + await expect(page.getByTestId('from-template-btn')).not.toBeVisible({ timeout: 3_000 }); + }); + + // NIT 2 — Empty-engagement SimulationList still shows dropdown + test('NIT-2 — engagement with 0 simulations still shows New simulation dropdown', async ({ + page, + context, + }) => { + // Create a fresh engagement with no simulations + const eng = await createEngagement(redteamToken, { + name: 'US27 empty eng dropdown', + start_date: '2026-01-01', + }); + + await seedTokenInStorage(context, redteamToken); + await page.goto(`/engagements/${eng.id}`); + + // Primary button visible even in empty state + await expect(page.getByTestId('new-simulation-btn')).toBeVisible({ timeout: 5_000 }); + + // Chevron also visible and functional + await page.getByTestId('new-simulation-dropdown-toggle').click(); + await expect(page.getByTestId('from-template-btn')).toBeVisible({ timeout: 3_000 }); + + const tok = await adminToken(); + await deleteEngagement(tok, eng.id); + }); });