/** * 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)); function detectRuntime(): string { try { execSync('command -v docker', { stdio: 'ignore' }); return 'docker'; } catch { return 'podman'; } } const RUNTIME = process.env.MIMIC_CONTAINER_CMD ?? detectRuntime(); 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 wraps an `exec … flask create-admin` // through the container engine. Sprint 2 made the engine configurable via // $(CONTAINER_CMD) (auto-detects docker or podman), so we assert the variable form. const makefilePath = resolve(__dirname, '../..', 'Makefile'); const content = readFileSync(makefilePath, 'utf8'); expect(content).toMatch(/\$\(CONTAINER_CMD\) exec .+ flask create-admin/); }); });