import { execFile } from "node:child_process"; import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { verifyPassword } from "../src/lib/admin-file"; /** Runs the CLI as a real process with piped (non-interactive) stdin. */ const run = promisify(execFile); const SCRIPT = path.join(import.meta.dirname, "create-admin.mjs"); const PASSWORD = "correct horse battery"; let dir: string; let file: string; beforeEach(async () => { dir = await mkdtemp(path.join(os.tmpdir(), "create-admin-")); file = path.join(dir, "admin.json"); }); afterEach(() => rm(dir, { recursive: true, force: true })); async function cli(args: string[], stdin: string) { const child = run( process.execPath, [SCRIPT, ...args], { env: { ...process.env, ADMIN_AUTH_FILE: file } }, ); child.child.stdin!.end(stdin); return child.then( ({ stdout, stderr }) => ({ code: 0, stdout, stderr }), (err) => ({ code: err.code as number, stdout: err.stdout as string, stderr: err.stderr as string }), ); } const readAdmin = async () => JSON.parse(await readFile(file, "utf8")); describe("onvif-dashboard admin", () => { it("creates an owner-only admin file with a hash of the piped password", async () => { const res = await cli(["--username", "admin"], `${PASSWORD}\n`); expect(res).toMatchObject({ code: 0, stderr: "" }); expect(res.stdout).toContain(`Admin "admin" saved to ${file} (owner-only).`); const admin = await readAdmin(); expect(admin.username).toBe("admin"); expect(await verifyPassword(PASSWORD, admin.passwordHash)).toBe(true); expect(JSON.stringify(admin)).not.toContain(PASSWORD); expect((await stat(file)).mode & 0o777).toBe(0o600); }); it("reads the username from stdin too", async () => { expect((await cli([], `ops\n${PASSWORD}\n`)).code).toBe(0); expect((await readAdmin()).username).toBe("ops"); }); it("won't replace an existing admin without --force", async () => { await cli(["--username", "admin"], `${PASSWORD}\n`); const res = await cli(["--username", "intruder"], `${PASSWORD}\n`); expect(res.code).toBe(1); expect(res.stderr).toContain('an admin ("admin") already exists'); expect((await readAdmin()).username).toBe("admin"); }); it("replaces the admin with --force and says sessions are signed out", async () => { await cli(["--username", "admin"], `${PASSWORD}\n`); const res = await cli(["--username", "admin", "--force"], "a brand new password\n"); expect(res.code).toBe(0); expect(res.stdout).toContain("Existing sessions are signed out"); expect(await verifyPassword("a brand new password", (await readAdmin()).passwordHash)).toBe(true); }); it.each([ ["a short password", ["--username", "admin"], "short\n", "password: At least 12 characters"], ["an invalid username", ["--username", "bad name"], `${PASSWORD}\n`, "username 1–64"], ["no input", [], "", "username 1–64"], ])("rejects %s and writes nothing", async (_label, args, stdin, message) => { const res = await cli(args, stdin); expect(res.code).toBe(1); expect(res.stderr).toContain(message); await expect(stat(file)).rejects.toThrow(/ENOENT/); }); it("refuses to run over a corrupt admin file", async () => { await writeFile(file, "{oops"); const res = await cli(["--username", "admin", "--force"], `${PASSWORD}\n`); expect(res.code).toBe(1); expect(res.stderr).toContain("is not valid JSON"); }); it("prints usage with --help", async () => { const res = await cli(["--help"], ""); expect(res).toMatchObject({ code: 0 }); expect(res.stdout).toContain("Usage: onvif-dashboard admin"); }); });