110 lines
4.3 KiB
TypeScript
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/);
|
||
|
|
});
|
||
|
|
});
|