feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* US-5 — UI respects DESIGN.md tokens, layout doesn't break at 1280×720,
|
|
|
|
|
|
* and loading/error/empty states are wired.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Pixel-perfect fidelity is NOT enforced — instead we assert that the
|
|
|
|
|
|
* tokenised classes from tailwind.config.ts are present and that the canvas
|
|
|
|
|
|
* has no horizontal overflow at the design viewport.
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
|
|
import {
|
|
|
|
|
|
adminToken,
|
|
|
|
|
|
deleteAllEngagements,
|
|
|
|
|
|
deleteUserByUsername,
|
|
|
|
|
|
ensureUser,
|
|
|
|
|
|
login,
|
|
|
|
|
|
} from '../fixtures/api';
|
|
|
|
|
|
import { seedTokenInStorage } from '../fixtures/auth';
|
|
|
|
|
|
|
|
|
|
|
|
const REDTEAM_USER = 'us5-redteam';
|
|
|
|
|
|
const PASS = 'us5-passw0rd';
|
|
|
|
|
|
|
|
|
|
|
|
test.describe('US-5 — DESIGN.md fidelity, responsive, states', () => {
|
|
|
|
|
|
let redteamToken: string;
|
|
|
|
|
|
|
|
|
|
|
|
test.beforeAll(async () => {
|
|
|
|
|
|
await ensureUser(REDTEAM_USER, PASS, 'redteam');
|
|
|
|
|
|
redteamToken = (await login(REDTEAM_USER, PASS)).token;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test.afterAll(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tok = await adminToken();
|
|
|
|
|
|
await deleteAllEngagements(tok);
|
|
|
|
|
|
await deleteUserByUsername(tok, REDTEAM_USER);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* noop */
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AC-5.1 — DESIGN.md tokens applied (Inter font, brand palette, named utilities)', async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
|
await page.goto('/engagements');
|
|
|
|
|
|
|
|
|
|
|
|
// Body font must resolve to a stack including "Inter".
|
|
|
|
|
|
const fontFamily = await page.evaluate(
|
|
|
|
|
|
() => window.getComputedStyle(document.body).fontFamily,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(fontFamily.toLowerCase()).toMatch(/inter/);
|
|
|
|
|
|
|
|
|
|
|
|
// Tailwind-compiled DESIGN palette: the primary chevron at the top-left of
|
|
|
|
|
|
// the nav uses `bg-primary` → rendered as rgb(2, 74, 216).
|
|
|
|
|
|
const chevronBg = await page
|
|
|
|
|
|
.locator('header a[aria-label="Mimic home"] span[aria-hidden]')
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.evaluate((el) => window.getComputedStyle(el).backgroundColor);
|
|
|
|
|
|
expect(chevronBg.replace(/\s/g, '')).toBe('rgb(2,74,216)');
|
|
|
|
|
|
|
test(e2e): sprint 4 acceptance tests — US-17 to US-23
Add new spec files for US-17 (UI polish), US-18 (done read-only + reopen),
US-19 (engagement auto-status), US-20 (matrix fits modal), US-21 (tactic
selection), US-22 (MITRE input redesign), US-23 (dark mode).
Adapt sprint 2/3 specs for sprint 4 UI renames: matrix icon button replaces
text buttons, inline search replaces Quick Search, Save replaces Save Red Team,
New replaces New Engagement, topbar uses bg-slab tokens, Apply N item(s) replaces
Apply N technique(s), done→review_required transition now valid (Reopen flow).
Mark AC-21.6 Apply-from-modal as test.fail: known defect where /api/mitre/matrix
returns slug tactic IDs but PATCH /simulations/:id expects TA-format IDs.
Final result: 156 passed, 0 failed (1 expected failure via test.fail).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:27:12 +02:00
|
|
|
|
// Sprint 4: topbar utility-strip uses `bg-slab` (#111827 → rgb(17,24,39)).
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
const utilityBg = await page
|
test(e2e): sprint 4 acceptance tests — US-17 to US-23
Add new spec files for US-17 (UI polish), US-18 (done read-only + reopen),
US-19 (engagement auto-status), US-20 (matrix fits modal), US-21 (tactic
selection), US-22 (MITRE input redesign), US-23 (dark mode).
Adapt sprint 2/3 specs for sprint 4 UI renames: matrix icon button replaces
text buttons, inline search replaces Quick Search, Save replaces Save Red Team,
New replaces New Engagement, topbar uses bg-slab tokens, Apply N item(s) replaces
Apply N technique(s), done→review_required transition now valid (Reopen flow).
Mark AC-21.6 Apply-from-modal as test.fail: known defect where /api/mitre/matrix
returns slug tactic IDs but PATCH /simulations/:id expects TA-format IDs.
Final result: 156 passed, 0 failed (1 expected failure via test.fail).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:27:12 +02:00
|
|
|
|
.locator('div.bg-slab.text-slab-text')
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
.first()
|
|
|
|
|
|
.evaluate((el) => window.getComputedStyle(el).backgroundColor);
|
test(e2e): sprint 4 acceptance tests — US-17 to US-23
Add new spec files for US-17 (UI polish), US-18 (done read-only + reopen),
US-19 (engagement auto-status), US-20 (matrix fits modal), US-21 (tactic
selection), US-22 (MITRE input redesign), US-23 (dark mode).
Adapt sprint 2/3 specs for sprint 4 UI renames: matrix icon button replaces
text buttons, inline search replaces Quick Search, Save replaces Save Red Team,
New replaces New Engagement, topbar uses bg-slab tokens, Apply N item(s) replaces
Apply N technique(s), done→review_required transition now valid (Reopen flow).
Mark AC-21.6 Apply-from-modal as test.fail: known defect where /api/mitre/matrix
returns slug tactic IDs but PATCH /simulations/:id expects TA-format IDs.
Final result: 156 passed, 0 failed (1 expected failure via test.fail).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:27:12 +02:00
|
|
|
|
expect(utilityBg.replace(/\s/g, '')).toBe('rgb(17,24,39)');
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
|
|
|
|
|
|
// Spot-check a few semantic class names live in the DOM (proves tokens are
|
|
|
|
|
|
// wired through tailwind.config.ts and not ad-hoc hex values).
|
|
|
|
|
|
await expect(page.locator('.btn-primary, .card-product, .max-w-page').first()).toBeVisible();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AC-5.2 — no horizontal overflow at 1280×720 across key pages', async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
|
|
|
|
|
|
|
|
|
|
const routes = ['/engagements', '/engagements/new'];
|
|
|
|
|
|
for (const route of routes) {
|
|
|
|
|
|
await page.goto(route);
|
|
|
|
|
|
const overflow = await page.evaluate(() => ({
|
|
|
|
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
|
|
|
|
clientWidth: document.documentElement.clientWidth,
|
|
|
|
|
|
}));
|
|
|
|
|
|
expect(
|
|
|
|
|
|
overflow.scrollWidth,
|
|
|
|
|
|
`${route} should not horizontally overflow at 1280px`,
|
|
|
|
|
|
).toBeLessThanOrEqual(overflow.clientWidth + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AC-5.3a — empty state renders when engagements list is empty', async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
|
const tok = await adminToken();
|
|
|
|
|
|
await deleteAllEngagements(tok);
|
|
|
|
|
|
await page.goto('/engagements');
|
|
|
|
|
|
await expect(page.getByRole('heading', { name: /no engagements yet/i })).toBeVisible();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AC-5.3b — loading state renders while engagements list is in-flight', async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
|
|
|
|
|
|
|
// Stall the list endpoint so the LoadingState becomes visible.
|
|
|
|
|
|
const handler = async (route: import('@playwright/test').Route) => {
|
|
|
|
|
|
const url = route.request().url();
|
|
|
|
|
|
if (/\/api\/engagements(\?.*)?$/.test(url)) {
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1500));
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await route.continue();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* route may have been torn down by the time we resume */
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
await page.route('**/api/engagements**', handler);
|
|
|
|
|
|
|
|
|
|
|
|
const navPromise = page.goto('/engagements');
|
|
|
|
|
|
await expect(page.getByText(/loading engagements/i)).toBeVisible({ timeout: 3_000 });
|
|
|
|
|
|
await navPromise;
|
|
|
|
|
|
await page.unroute('**/api/engagements**', handler);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AC-5.3c — error state renders when the engagements list returns 500', async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
|
|
|
|
|
|
|
const handler = async (route: import('@playwright/test').Route) => {
|
|
|
|
|
|
const url = route.request().url();
|
|
|
|
|
|
if (/\/api\/engagements(\?.*)?$/.test(url)) {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 500,
|
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
|
body: JSON.stringify({ error: 'Forced failure' }),
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await route.continue();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* route teardown race — harmless */
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
await page.route('**/api/engagements**', handler);
|
|
|
|
|
|
|
|
|
|
|
|
await page.goto('/engagements');
|
|
|
|
|
|
await expect(page.getByTestId('error-state')).toBeVisible({ timeout: 8_000 });
|
|
|
|
|
|
await expect(page.getByTestId('error-state')).toContainText(/forced failure/i);
|
|
|
|
|
|
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible();
|
|
|
|
|
|
await page.unroute('**/api/engagements**', handler);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|