185 lines
6.8 KiB
TypeScript
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");
|
|
});
|