cameras/cli/onvif-dashboard.test.ts
Michael Mainguy 30b1f36e1b Ship a standalone build and an onvif-dashboard start command
- next.config.ts builds standalone output with image optimization off,
  and scripts/bundle-standalone.mjs puts the static files beside the
  server, then drops what tracing swept in but the app never runs (the
  MediaMTX binary and sharp's platform binaries, 79 MB). The build is
  27 MB and npm pack is 4.9 MB.
- The CLI gained start, which runs that server from any directory with
  the remembered data folder and stored key in its environment, and
  admin, so the messages in the UI can name a command that exists.
- Messages that said "npm run …" now say "onvif-dashboard …".

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

74 lines
2.8 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdtemp, readFile, realpath, rm, stat } 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";
/** Runs the published command as a real process, in a directory of its own. */
const run = promisify(execFile);
const CLI = path.join(import.meta.dirname, "onvif-dashboard.mjs");
let dir: string;
beforeEach(async () => {
// realpath because macOS temp folders are symlinks, and the CLI reports resolved paths.
dir = await realpath(await mkdtemp(path.join(os.tmpdir(), "cli-")));
});
afterEach(() => rm(dir, { recursive: true, force: true }));
const cli = (args: string[]) =>
run(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", CLI, ...args], {
cwd: dir,
env: {
...process.env,
XDG_CONFIG_HOME: path.join(dir, "config"),
CAMERAS_BIN_DIR: path.join(dir, "bin"),
CAMERAS_DATA_DIR: undefined,
},
}).catch((err: Error & { code?: number; stdout?: string; stderr?: string }) => ({
code: err.code,
stdout: err.stdout ?? "",
stderr: err.stderr ?? "",
}));
describe("onvif-dashboard", () => {
it("explains itself, and says so when the command is unknown", async () => {
expect((await cli(["help"])).stdout).toContain("onvif-dashboard setup");
const bad = (await cli(["wat"])) as { code?: number; stderr: string };
expect(bad.code).toBe(2);
expect(bad.stderr).toContain("Unknown command: wat");
});
it("sets a machine up without a terminal: default folder, a key, and what to do next", async () => {
const { stdout } = await cli(["setup"]);
const data = path.join(dir, ".data");
expect(stdout).toContain("No terminal to ask");
expect(stdout).toContain(`Data folder: ${data}`);
expect(stdout).toContain("install-video");
expect(stdout).toContain("No admin login yet");
expect(stdout).toContain("onvif-dashboard start");
const secrets = path.join(data, "secrets.env");
expect((await stat(secrets)).mode & 0o777).toBe(0o600);
expect(await readFile(secrets, "utf8")).toMatch(/CAMERA_CREDENTIALS_KEY=[0-9a-f]{64}/);
expect(JSON.parse(await readFile(path.join(dir, "config/onvif-dashboard/config.json"), "utf8"))).toEqual({
dataDir: data,
});
});
it("offers the admin command the messages point people at", async () => {
const { stdout } = await cli(["admin", "--help"]);
expect(stdout).toContain("--force");
});
it("running it again changes nothing", async () => {
await cli(["setup"]);
const secrets = path.join(dir, ".data", "secrets.env");
const before = await readFile(secrets, "utf8");
const { stdout } = await cli(["setup"]);
expect(await readFile(secrets, "utf8")).toBe(before);
expect(stdout).not.toContain("Made a key");
});
});