Files
NixMsg/sdk/js/test/harness.ts
T

199 lines
5.7 KiB
TypeScript

import { spawn, execFileSync } from "node:child_process";
import { createWriteStream, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { setTimeout as sleep } from "node:timers/promises";
const __dirname = dirname(fileURLToPath(import.meta.url));
export type ServerInfo = {
httpBase: string;
adminHttpBase: string;
mqttWs: string;
adminPassword: string;
stop: () => Promise<void>;
};
let cachedBin: string | undefined;
function findRepoRoot(): string {
let dir = __dirname;
for (;;) {
const mod = join(dir, "go.mod");
const cmd = join(dir, "cmd", "nixmsg");
if (existsSync(mod) && existsSync(cmd)) {
const text = readFileSync(mod, "utf8");
if (text.includes("module git.asio.asia/nixevol/NixMsg")) return dir;
}
const parent = dirname(dir);
if (parent === dir) throw new Error("repo root not found");
dir = parent;
}
}
function nixmsgBin(root: string): string {
if (cachedBin && existsSync(cachedBin)) return cachedBin;
const outDir = mkdtempSync(join(tmpdir(), "nixmsg-sdk-js-bin-"));
const name = process.platform === "win32" ? "nixmsg.exe" : "nixmsg";
const out = join(outDir, name);
execFileSync("go", ["build", "-o", out, "./cmd/nixmsg"], {
cwd: root,
env: { ...process.env, CGO_ENABLED: "0" },
stdio: ["ignore", "pipe", "pipe"],
});
cachedBin = out;
return out;
}
function parseAdminPassword(text: string): string {
for (const line of text.split(/\r?\n/)) {
const s = line.trim();
const lower = s.toLowerCase();
if (lower.startsWith("admin password:")) return s.slice("admin password:".length).trim();
if (lower.startsWith("password:")) return s.slice("password:".length).trim();
}
return "";
}
async function waitAddr(path: string, ms: number): Promise<string> {
const deadline = Date.now() + ms;
let last = "";
while (Date.now() < deadline) {
try {
const addr = readFileSync(path, "utf8").trim();
if (addr) return addr;
last = "empty";
} catch (e) {
last = String(e);
}
await sleep(20);
}
throw new Error(`wait listen.addr: ${last}`);
}
export async function startNixmsg(): Promise<ServerInfo> {
const root = findRepoRoot();
const bin = nixmsgBin(root);
const dataDir = mkdtempSync(join(tmpdir(), "nixmsg-sdk-js-itest-"));
const cfgPath = join(dataDir, "config.yaml");
const slash = dataDir.replace(/\\/g, "/");
writeFileSync(cfgPath, `listen: "127.0.0.1:0"\ndata_dir: "${slash}"\n`);
const init = execFileSync(bin, ["admin", "init"], {
env: { ...process.env, NIXMSG_CONFIG: cfgPath },
encoding: "utf8",
});
const adminPassword = parseAdminPassword(init);
if (!adminPassword) throw new Error(`admin init no password:\n${init}`);
const child = spawn(bin, ["serve"], {
env: { ...process.env, NIXMSG_CONFIG: cfgPath },
stdio: ["ignore", "pipe", "pipe"],
});
const log = createWriteStream(join(dataDir, "serve.log"));
child.stdout?.pipe(log);
child.stderr?.pipe(log);
let stopped = false;
const stop = async () => {
if (stopped) return;
stopped = true;
if (child.pid) {
try {
child.kill();
} catch {
/* ignore */
}
await new Promise<void>((r) => child.once("exit", () => r()));
}
try {
rmSync(dataDir, { recursive: true, force: true });
} catch {
/* ignore */
}
};
try {
const addr = await waitAddr(join(dataDir, "listen.addr"), 20000);
const httpBase = `http://${addr}`;
return {
httpBase,
adminHttpBase: httpBase,
mqttWs: `ws://${addr}/mqtt`,
adminPassword,
stop,
};
} catch (e) {
await stop();
throw e;
}
}
export class AdminClient {
private cookie = "";
constructor(
private base: string,
private password: string,
) {}
async login(): Promise<void> {
const res = await fetch(`${this.base}/api/admin/login`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ username: "admin", password: this.password }),
});
const raw = await res.text();
if (!res.ok) throw new Error(`admin login ${res.status} ${raw}`);
const set = res.headers.getSetCookie?.() ?? [];
for (const c of set) {
const m = /^nixmsg_admin=([^;]+)/.exec(c);
if (m) this.cookie = m[1];
}
if (!this.cookie) {
// Node fetch may expose set-cookie differently
const sc = res.headers.get("set-cookie");
if (sc) {
const m = /nixmsg_admin=([^;]+)/.exec(sc);
if (m) this.cookie = m[1];
}
}
if (!this.cookie) throw new Error("admin cookie missing");
}
private headers(json = true): Record<string, string> {
const h: Record<string, string> = {
cookie: `nixmsg_admin=${this.cookie}`,
"X-Nixmsg-Request": "1",
};
if (json) h["content-type"] = "application/json";
return h;
}
async putRegistration(enabled: boolean, code?: string): Promise<void> {
const body: Record<string, unknown> = { enabled };
if (code) body.code = code;
const res = await fetch(`${this.base}/api/admin/registration`, {
method: "PUT",
headers: this.headers(),
body: JSON.stringify(body),
});
const raw = await res.text();
if (!res.ok) throw new Error(`registration put ${res.status} ${raw}`);
}
async kick(id: string): Promise<void> {
const res = await fetch(`${this.base}/api/admin/endpoints/${id}/kick`, {
method: "POST",
headers: this.headers(),
body: "{}",
});
const raw = await res.text();
if (!res.ok) throw new Error(`kick ${id} ${res.status} ${raw}`);
}
}
export function delay0(): number {
return 0;
}