import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; const NODE_VERSION = "22.23.2"; const RUNTIME_NAME = `node-v${NODE_VERSION}-test`; const RUNTIME_ARCHIVE = `${RUNTIME_NAME}.tar.gz`; test("POSIX bootstrap installs a private Node and falls back after a network failure", { skip: process.platform === "win32", }, () => { const fixture = createPosixFixture(); try { const result = runPosixFixture(fixture, "fallback"); assert.equal(result.status, 0, result.stderr); assert.deepEqual(JSON.parse(result.stdout.trim()), { ok: true, status: "installed", managedNode: true }); assert.match(result.stderr, /\[dingtalk-approval\] 1\/4[\s\S]*2\/4[\s\S]*3\/4[\s\S]*4\/4/); assert.equal(existsSync(join(fixture.clientRoot, "node", RUNTIME_NAME, "bin", "node")), true); const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/); assert.deepEqual(calls.slice(0, 2), [ `https://primary.example.test/node/${RUNTIME_ARCHIVE}`, `https://mirror.example.test/node/${RUNTIME_ARCHIVE}`, ]); assert.ok(calls.includes("https://release.example.test/installer.mjs")); assert.ok(calls.includes("https://release.example.test/release-signing-public.pem")); } finally { rmSync(fixture.root, { recursive: true, force: true }); } }); test("POSIX bootstrap stops immediately when a downloaded runtime has the wrong hash", { skip: process.platform === "win32", }, () => { const fixture = createPosixFixture(); try { const result = runPosixFixture(fixture, "corrupt"); assert.notEqual(result.status, 0); assert.match(result.stderr, /SHA-256 不匹配,停止安装/); const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/); assert.deepEqual(calls, [`https://primary.example.test/node/${RUNTIME_ARCHIVE}`]); assert.equal(existsSync(join(fixture.clientRoot, "node", RUNTIME_NAME, "bin", "node")), false); } finally { rmSync(fixture.root, { recursive: true, force: true }); } }); test("POSIX bootstrap does not hide a non-404 HTTP error by switching mirrors", { skip: process.platform === "win32", }, () => { const fixture = createPosixFixture(); try { const result = runPosixFixture(fixture, "server-error"); assert.notEqual(result.status, 0); assert.match(result.stderr, /不允许换源.*HTTP 500/); const calls = readFileSync(fixture.callLog, "utf8").trim().split(/\r?\n/); assert.deepEqual(calls, [`https://primary.example.test/node/${RUNTIME_ARCHIVE}`]); } finally { rmSync(fixture.root, { recursive: true, force: true }); } }); test("Windows bootstrap is valid PowerShell after release rendering", { skip: process.platform !== "win32", }, () => { const root = mkdtempSync(join(tmpdir(), "dingmcp-windows-bootstrap-")); try { const script = join(root, "bootstrap.ps1"); writeFileSync(script, renderWindowsBootstrap()); const powershell = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); const parser = [ "$tokens = $null; $errors = $null", "[System.Management.Automation.Language.Parser]::ParseFile($args[0], [ref]$tokens, [ref]$errors) | Out-Null", "if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_.Message }; exit 1 }", ].join("; "); const result = spawnSync(powershell, ["-NoProfile", "-NonInteractive", "-Command", parser, script], { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); } finally { rmSync(root, { recursive: true, force: true }); } }); interface PosixFixture { root: string; clientRoot: string; callLog: string; script: string; fakeBin: string; archive: string; installer: string; publicKey: string; } function createPosixFixture(): PosixFixture { const root = mkdtempSync(join(tmpdir(), "dingmcp-posix-bootstrap-")); const archiveRoot = join(root, "archive", RUNTIME_NAME); const fakeBin = join(root, "bin"); const clientRoot = join(root, "home", "dingtalk-approval"); const callLog = join(root, "download-calls.log"); const archive = join(root, RUNTIME_ARCHIVE); const installer = join(root, "installer.mjs"); const publicKey = join(root, "release-signing-public.pem"); const script = join(root, "bootstrap.sh"); mkdirSync(join(archiveRoot, "bin"), { recursive: true }); mkdirSync(join(archiveRoot, "lib", "node_modules", "npm", "bin"), { recursive: true }); mkdirSync(fakeBin, { recursive: true }); mkdirSync(join(root, "home"), { recursive: true }); const privateNode = join(archiveRoot, "bin", "node"); writeFileSync(privateNode, [ "#!/bin/sh", "if [ \"${1:-}\" = '-e' ]; then exit 0; fi", "printf '%s\\n' '{\"ok\":true,\"status\":\"installed\",\"managedNode\":true}'", "", ].join("\n")); chmodSync(privateNode, 0o700); writeFileSync(join(archiveRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js"), "// fixture\n"); const tar = spawnSync("tar", ["-czf", archive, "-C", join(root, "archive"), RUNTIME_NAME], { encoding: "utf8" }); assert.equal(tar.status, 0, tar.stderr); writeFileSync(installer, "// fixture installer\n"); writeFileSync(publicKey, "fixture public key\n"); writeFileSync(callLog, ""); const curl = join(fakeBin, "curl"); writeFileSync(curl, `#!/bin/sh ding_target='' ding_url='' while [ "$#" -gt 0 ]; do case "$1" in --output) ding_target="$2"; shift 2 ;; http*) ding_url="$1"; shift ;; *) shift ;; esac done printf '%s\\n' "$ding_url" >> "$DING_TEST_CALL_LOG" case "$ding_url" in https://primary.example.test/node/*) if [ "$DING_TEST_MODE" = 'corrupt' ]; then printf 'corrupt' > "$ding_target"; printf '200'; exit 0; fi if [ "$DING_TEST_MODE" = 'server-error' ]; then printf '500'; exit 22; fi printf '000'; exit 7 ;; https://mirror.example.test/node/*) /bin/cp "$DING_TEST_ARCHIVE" "$ding_target"; printf '200' ;; https://official.example.test/node/*) /bin/cp "$DING_TEST_ARCHIVE" "$ding_target"; printf '200' ;; https://release.example.test/installer.mjs) /bin/cp "$DING_TEST_INSTALLER" "$ding_target"; printf '200' ;; https://release.example.test/release-signing-public.pem) /bin/cp "$DING_TEST_PUBLIC_KEY" "$ding_target"; printf '200' ;; *) printf '404'; exit 22 ;; esac `); chmodSync(curl, 0o700); const rejectedSystemNode = join(fakeBin, "node"); writeFileSync(rejectedSystemNode, "#!/bin/sh\nexit 1\n"); chmodSync(rejectedSystemNode, 0o700); let body = readFileSync("distribution/bootstrap-posix.template.sh", "utf8"); const replacements: Record = { "{{NODE_SOURCE_BASES_POSIX}}": "https://primary.example.test/node/ https://mirror.example.test/node/ https://official.example.test/node/", "{{INSTALLER_URL}}": "https://release.example.test/installer.mjs", "{{INSTALLER_SHA256}}": digest(installer), "{{PUBLIC_KEY_URL}}": "https://release.example.test/release-signing-public.pem", "{{PUBLIC_KEY_SHA256}}": digest(publicKey), "{{MANIFEST_URL}}": "https://release.example.test/manifest.json", }; for (const platform of ["LINUX_X64", "LINUX_ARM64", "DARWIN_X64", "DARWIN_ARM64"]) { replacements[`{{NODE_${platform}_FILE}}`] = RUNTIME_ARCHIVE; replacements[`{{NODE_${platform}_SHA256}}`] = digest(archive); } for (const [placeholder, value] of Object.entries(replacements)) body = body.replaceAll(placeholder, value); assert.doesNotMatch(body, /\{\{/); writeFileSync(script, body); chmodSync(script, 0o700); return { root, clientRoot, callLog, script, fakeBin, archive, installer, publicKey }; } function runPosixFixture(fixture: PosixFixture, mode: "fallback" | "corrupt" | "server-error") { return spawnSync("/bin/sh", [fixture.script], { encoding: "utf8", env: { HOME: join(fixture.root, "home"), PATH: `${fixture.fakeBin}:/usr/bin:/bin`, DINGTALK_APPROVAL_CLIENT_HOME: fixture.clientRoot, DING_TEST_MODE: mode, DING_TEST_CALL_LOG: fixture.callLog, DING_TEST_ARCHIVE: fixture.archive, DING_TEST_INSTALLER: fixture.installer, DING_TEST_PUBLIC_KEY: fixture.publicKey, }, }); } function renderWindowsBootstrap(): string { let body = readFileSync("distribution/bootstrap-windows.template.ps1", "utf8"); const replacements: Record = { "{{NODE_SOURCE_BASES_POWERSHELL}}": '"https://primary.example.test/node/", "https://official.example.test/node/"', "{{INSTALLER_URL}}": "https://release.example.test/installer.mjs", "{{INSTALLER_SHA256}}": "a".repeat(64), "{{PUBLIC_KEY_URL}}": "https://release.example.test/release-signing-public.pem", "{{PUBLIC_KEY_SHA256}}": "b".repeat(64), "{{MANIFEST_URL}}": "https://release.example.test/manifest.json", }; for (const platform of ["WIN32_X64", "WIN32_ARM64"]) { replacements[`{{NODE_${platform}_FILE}}`] = `node-v${NODE_VERSION}-win-x64.zip`; replacements[`{{NODE_${platform}_SHA256}}`] = "c".repeat(64); } for (const [placeholder, value] of Object.entries(replacements)) body = body.replaceAll(placeholder, value); assert.doesNotMatch(body, /\{\{/); return body; } function digest(path: string): string { return createHash("sha256").update(readFileSync(path)).digest("hex"); }