/** * Thin axios client used by tests to seed/teardown users and engagements * without going through the UI. The bootstrap admin is created out-of-band * (via `make create-admin`) and logs in once to provision per-suite users. */ import axios, { AxiosInstance, isAxiosError } from 'axios'; export interface User { id: number; username: string; role: 'admin' | 'redteam' | 'soc'; created_at: string | null; } export interface Engagement { id: number; name: string; description: string | null; start_date: string; end_date: string | null; status: 'planned' | 'active' | 'closed'; created_at: string | null; created_by: { id: number; username: string } | null; } export type Role = User['role']; const BASE_URL = process.env.MIMIC_BASE_URL ?? 'http://localhost:5000'; const BOOTSTRAP_ADMIN_USER = process.env.MIMIC_BOOTSTRAP_USER ?? 'root'; const BOOTSTRAP_ADMIN_PASS = process.env.MIMIC_BOOTSTRAP_PASS ?? 'rootpass8'; export function makeClient(token?: string): AxiosInstance { return axios.create({ baseURL: `${BASE_URL}/api`, headers: token ? { Authorization: `Bearer ${token}` } : undefined, validateStatus: () => true, // tests assert on status themselves }); } export async function login( username: string, password: string, ): Promise<{ token: string; user: User }> { const client = makeClient(); const r = await client.post('/auth/login', { username, password }); if (r.status !== 200) { throw new Error(`login(${username}) failed: ${r.status} ${JSON.stringify(r.data)}`); } return { token: r.data.access_token as string, user: r.data.user as User }; } export async function adminToken(): Promise { const { token } = await login(BOOTSTRAP_ADMIN_USER, BOOTSTRAP_ADMIN_PASS); return token; } /** * Idempotent helper: ensures a user with the given username/role exists and * has the requested password. Returns the user record. * * Strategy: * - try login: if it succeeds, we're done. * - else: as admin, list users; if username found, PATCH password+role; else POST. */ export async function ensureUser( username: string, password: string, role: Role, ): Promise { try { const { user } = await login(username, password); if (user.role !== role) { const admin = await adminToken(); const client = makeClient(admin); const r = await client.patch(`/users/${user.id}`, { role }); if (r.status !== 200) throw new Error(`patch role: ${r.status}`); return r.data as User; } return user; } catch { // fall through to admin path } const admin = await adminToken(); const client = makeClient(admin); const list = await client.get('/users'); if (list.status !== 200) { throw new Error(`list users failed: ${list.status} ${JSON.stringify(list.data)}`); } const existing = (list.data as User[]).find((u) => u.username === username); if (existing) { const r = await client.patch(`/users/${existing.id}`, { password, role }); if (r.status !== 200) { throw new Error(`patch user failed: ${r.status} ${JSON.stringify(r.data)}`); } return r.data as User; } const r = await client.post('/users', { username, password, role }); if (r.status !== 201) { throw new Error(`create user failed: ${r.status} ${JSON.stringify(r.data)}`); } return r.data as User; } export async function deleteUserByUsername(token: string, username: string): Promise { const client = makeClient(token); const list = await client.get('/users'); if (list.status !== 200) return; const u = (list.data as User[]).find((x) => x.username === username); if (!u) return; await client.delete(`/users/${u.id}`); } export async function createEngagement( token: string, payload: Partial>, ): Promise { const client = makeClient(token); const body = { name: payload.name ?? 'Test Engagement', description: payload.description, start_date: payload.start_date ?? '2026-01-01', end_date: payload.end_date, status: payload.status ?? 'planned', }; const r = await client.post('/engagements', body); if (r.status !== 201) { throw new Error(`create engagement failed: ${r.status} ${JSON.stringify(r.data)}`); } return r.data as Engagement; } export async function deleteEngagement(token: string, id: number): Promise { const client = makeClient(token); await client.delete(`/engagements/${id}`); } export async function listEngagements(token: string): Promise { const client = makeClient(token); const r = await client.get('/engagements'); if (r.status !== 200) { throw new Error(`list engagements failed: ${r.status}`); } return r.data as Engagement[]; } export async function deleteAllEngagements(token: string): Promise { const items = await listEngagements(token); await Promise.all(items.map((e) => deleteEngagement(token, e.id))); } export async function waitForHealth(timeoutMs = 30_000): Promise { const deadline = Date.now() + timeoutMs; const client = makeClient(); let lastErr: unknown = null; while (Date.now() < deadline) { try { const r = await client.get('/health'); if (r.status === 200) return; } catch (e) { lastErr = e; } await new Promise((r) => setTimeout(r, 500)); } throw new Error( `backend not healthy after ${timeoutMs}ms: ${ isAxiosError(lastErr) ? lastErr.message : String(lastErr) }`, ); } export const BASE = BASE_URL;