test: Go/JS SDK 接入清单集成测与 README 示例
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user