Files
mimic/e2e/tests/us1-bootstrap-admin.spec.ts
Knacky 5104f7c429 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

110 lines
4.3 KiB
TypeScript

/**
* US-1 — bootstrap the first admin via `flask create-admin`.
*
* The `make create-admin` target wraps `docker exec mimic flask create-admin …`.
* These tests exercise the CLI directly through `docker exec` (or whatever
* runtime the harness exposes via the wrapper), and a follow-up API login to
* confirm the row was created with role=admin and an argon2 hash that verifies.
*
* NOTE: the bootstrap admin (`root` / `rootpass8`) is already created out-of-band
* before the Playwright suite starts. Test usernames here are scoped to `us1-*`
* and cleaned up via API in afterAll.
*/
import { test, expect } from '@playwright/test';
import { execSync } from 'node:child_process';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { adminToken, deleteUserByUsername, login, makeClient } from '../fixtures/api';
const __dirname = dirname(fileURLToPath(import.meta.url));
const RUNTIME = process.env.MIMIC_CONTAINER_CMD ?? 'docker';
const CONTAINER = process.env.MIMIC_CONTAINER ?? 'mimic';
function runCreateAdmin(user: string, pass: string): {
stdout: string;
stderr: string;
status: number;
} {
try {
const stdout = execSync(`${RUNTIME} exec ${CONTAINER} flask create-admin ${user} ${pass}`, {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
});
return { stdout, stderr: '', status: 0 };
} catch (e) {
const err = e as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
return {
stdout: typeof err.stdout === 'string' ? err.stdout : err.stdout?.toString() ?? '',
stderr: typeof err.stderr === 'string' ? err.stderr : err.stderr?.toString() ?? '',
status: err.status ?? -1,
};
}
}
test.describe('US-1 — bootstrap first admin', () => {
const probeUser = 'us1-probe-admin';
const dupUser = 'us1-dup-admin';
test.afterAll(async () => {
try {
const token = await adminToken();
for (const u of [probeUser, dupUser]) {
await deleteUserByUsername(token, u);
}
} catch {
/* best-effort cleanup */
}
});
test('AC-1.1 — create-admin creates a user with role=admin and an argon2 hash that authenticates', async () => {
const probePass = 'probepass123';
const result = runCreateAdmin(probeUser, probePass);
expect(result.status, `CLI failed: ${result.stderr || result.stdout}`).toBe(0);
expect(result.stdout).toMatch(/created/i);
// Roundtrip: the resulting credentials must log in as role=admin.
const { user } = await login(probeUser, probePass);
expect(user.username).toBe(probeUser);
expect(user.role).toBe('admin');
});
test('AC-1.2 — fails cleanly when username already exists', async () => {
// Seed once, then call again.
runCreateAdmin(dupUser, 'firstpass8');
const second = runCreateAdmin(dupUser, 'secondpass8');
expect(second.status).not.toBe(0);
const combined = (second.stderr + second.stdout).toLowerCase();
expect(combined).toMatch(/exists|already|error/);
});
test('AC-1.3 — refuses passwords shorter than 8 characters', async () => {
const result = runCreateAdmin('us1-shortpass-user', 'short7');
expect(result.status).not.toBe(0);
const combined = (result.stderr + result.stdout).toLowerCase();
expect(combined).toMatch(/8|length|password/);
// Make sure the short-password attempt did NOT create a row.
const probe = await makeClient().post('/auth/login', {
username: 'us1-shortpass-user',
password: 'short7',
});
expect(probe.status).toBe(401);
});
test('AC-1.4 — make create-admin wraps `docker exec mimic flask create-admin` (the bootstrap admin proves it ran via that path)', async () => {
// The harness step `make create-admin USER=root PASS=rootpass8` is what
// got the suite to the point where adminToken() works. If that call had
// been broken, this assertion would have already failed when seeding.
const token = await adminToken();
expect(token).toBeTruthy();
// Defence-in-depth: assert the Makefile target literally invokes
// `docker exec … flask create-admin`. This is a contract check.
const makefilePath = resolve(__dirname, '../..', 'Makefile');
const content = readFileSync(makefilePath, 'utf8');
expect(content).toMatch(/docker exec .+ flask create-admin/);
});
});