CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
216 lines
9.3 KiB
TypeScript
216 lines
9.3 KiB
TypeScript
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<string, string> = {
|
|
"{{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<string, string> = {
|
|
"{{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");
|
|
}
|