Files
2026-07-13 13:39:12 +08:00

185 lines
6.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execSync } from "node:child_process";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-installer-"));
const FAKE_BIN_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-fake-bin-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Prepend fake bin dir to PATH so our fake `npm` is found by runNpm
const originalPath = process.env.PATH ?? "";
process.env.PATH = `${FAKE_BIN_DIR}:${originalPath}`;
// Fake npm script: for `install` — creates expected package.json;
// for `view` — prints a version; for other args — no-op.
const INSTALL_DIR = path.join(TEST_DATA_DIR, "services", "9router");
const fakeNpmScript = `#!/bin/sh
set -e
CMD="$1"
shift
if [ "$CMD" = "install" ]; then
# Resolve the install prefix like real npm: an explicit --prefix arg wins,
# otherwise fall back to the npm_config_prefix env var (#5379 passes the
# prefix via env instead of argv so paths with spaces survive the win shell).
PREFIX=""
while [ $# -gt 0 ]; do
if [ "$1" = "--prefix" ]; then PREFIX="$2"; shift 2; else shift; fi
done
if [ -z "$PREFIX" ]; then PREFIX="$npm_config_prefix"; fi
PKG_DIR="$PREFIX/node_modules/9router"
mkdir -p "$PKG_DIR/app"
echo '{"name":"9router","version":"0.4.59"}' > "$PKG_DIR/package.json"
touch "$PKG_DIR/app/server.js"
exit 0
fi
if [ "$CMD" = "view" ]; then
echo "0.4.59"
exit 0
fi
exit 0
`;
const fakeNpmPath = path.join(FAKE_BIN_DIR, "npm");
fs.writeFileSync(fakeNpmPath, fakeNpmScript, { mode: 0o755 });
// Verify fake npm is on PATH
execSync("which npm", { env: process.env });
// DB bootstrap (must be before ninerouter import due to db/core eager init)
const core = await import("../../../../src/lib/db/core.ts");
const db = core.getDbInstance();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('9router', 'not_installed', 20130, 0, 1, 1)`
).run();
const {
install,
update,
uninstall,
getInstalledVersion,
getLatestVersion,
resolveSpawnArgs,
NINEROUTER_INSTALL_DIR,
} = await import("../../../../src/lib/services/installers/ninerouter.ts");
test.after(() => {
process.env.PATH = originalPath;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.rmSync(FAKE_BIN_DIR, { recursive: true, force: true });
});
test("install creates package.json structure", async () => {
const result = await install("0.4.59");
// Host package.json must exist
const hostPkg = path.join(NINEROUTER_INSTALL_DIR, "package.json");
assert.ok(fs.existsSync(hostPkg), "host package.json should exist");
const parsedHost = JSON.parse(fs.readFileSync(hostPkg, "utf8")) as {
name: string;
private: boolean;
};
assert.equal(parsedHost.name, "omniroute-9router-host");
assert.ok(parsedHost.private);
assert.equal(result.installedVersion, "0.4.59");
assert.equal(result.installPath, NINEROUTER_INSTALL_DIR);
assert.ok(result.durationMs >= 0);
});
test("install captures real version from node_modules/9router/package.json", async () => {
const ver = await getInstalledVersion();
assert.equal(ver, "0.4.59", "should read version from installed package");
});
test("update calls npm install with latest (idempotent)", async () => {
const result = await update();
assert.equal(result.installedVersion, "0.4.59");
});
test("uninstall removes node_modules and marks not_installed in DB", async () => {
const nmDir = path.join(NINEROUTER_INSTALL_DIR, "node_modules");
// Ensure node_modules exists from previous install tests
assert.ok(fs.existsSync(nmDir), "node_modules should exist before uninstall");
await uninstall();
assert.ok(!fs.existsSync(nmDir), "node_modules should be removed");
const { getVersionManagerTool } = await import("../../../../src/lib/db/versionManager.ts");
const row = await getVersionManagerTool("9router");
assert.equal(row?.status, "not_installed");
assert.equal(row?.installedVersion, null);
assert.equal(row?.binaryPath, null);
// package.json host file should remain (preserves metadata)
assert.ok(
fs.existsSync(path.join(NINEROUTER_INSTALL_DIR, "package.json")),
"host package.json should be kept after uninstall"
);
});
test("getLatestVersion returns version string from npm view", async () => {
const ver = await getLatestVersion();
assert.equal(ver, "0.4.59");
});
test("resolveSpawnArgs returns expected env and command", () => {
const args = resolveSpawnArgs("sk-test-api-key", 20130);
assert.equal(args.command, process.execPath, "command must be current node binary");
assert.equal(args.args[0], "--max-old-space-size=6144");
assert.ok(args.args[1]?.includes("server.js"), "args[1] should point to server.js");
assert.equal(args.env.PORT, "20130");
assert.equal(args.env.HOSTNAME, "127.0.0.1");
assert.equal(args.env.API_KEY_SECRET, "sk-test-api-key");
assert.ok(args.env.DATA_DIR?.includes("9router"), "DATA_DIR should be scoped to 9router");
assert.equal(args.env.NODE_ENV, "production");
assert.equal(args.env.DISABLE_MITM, "true");
assert.equal(args.env.DISABLE_TUNNEL, "true");
});
test("EACCES error returns friendly InstallError", async () => {
const { InstallError } = await import("../../../../src/lib/services/installers/utils.ts");
// Simulate EACCES by making the install dir read-only
const lockedDir = path.join(TEST_DATA_DIR, "locked-services", "9router");
fs.mkdirSync(lockedDir, { recursive: true });
fs.chmodSync(path.join(TEST_DATA_DIR, "locked-services"), 0o444);
process.env.DATA_DIR = path.join(TEST_DATA_DIR, "locked-services");
try {
await import("../../../../src/lib/services/installers/ninerouter.ts?locked=1");
} catch {
// Re-import after DATA_DIR change fails due to caching — that's fine
}
process.env.DATA_DIR = TEST_DATA_DIR;
// Restore for cleanup
fs.chmodSync(path.join(TEST_DATA_DIR, "locked-services"), 0o755);
// At minimum, InstallError should carry httpStatus and friendly fields
const err = new InstallError("raw error", "friendly message", 403);
assert.equal(err.httpStatus, 403);
assert.equal(err.friendly, "friendly message");
assert.ok(err instanceof Error);
assert.equal(err.name, "InstallError");
});
test("InstallError timeout shape has correct httpStatus 504", async () => {
const { InstallError: IE } = await import("../../../../src/lib/services/installers/utils.ts");
const err = new IE("npm process killed", "Instalação demorou demais. Tente novamente.", 504);
assert.equal(err.httpStatus, 504);
assert.equal(err.friendly, "Instalação demorou demais. Tente novamente.");
assert.ok(err instanceof Error);
assert.equal(err.name, "InstallError");
});