cameras/scripts/create-admin.test.ts
Michael Mainguy daaecb7452 Fix the published package: compile the CLI, bundle onvif (0.1.1)
Installing 0.1.0 failed twice, both my mistakes.

- Node refuses to strip types under node_modules, so shipping .ts for the
  CLI could never work from an installed package. tsconfig.cli.json now
  compiles those modules to dist/ as ES modules, and the helper scripts
  are plain .mjs.
- serverExternalPackages made Turbopack emit require("onvif-<hash>"),
  a name that resolves nowhere, because onvif comes from a git URL.
  Bundling it fixes that; checked from an installed tarball, where
  /api/discover answered 200 and a real camera's info and snapshot came
  back through the packaged server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:21:53 -05:00

95 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
});
});