Files
NetMesh/electron/bridges/systemSshAgent.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

284 lines
8.6 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const { utils } = require("ssh2");
const {
prepareSystemSshAgent,
resolveIdentityPath,
} = require("./systemSshAgent.cjs");
const TEST_PUBLIC_KEYS = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKxGkTKkraRFbFuzZ2hIJiiRCVecNR1V0Az2YPSHyB1",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKX948eIUbsCfpX6+wkEedowUaDcg9jZP0rAlCLfiPPY",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDTqolh2DHp5OhKylW13C0cZSwJ0pxzAWYoLsZR9VKE",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGwNYEIS5fjDhPYDAeKd+osXu05BTlVfvlBGTpctoaWj",
];
let nextTestPublicKey = 0;
function makePublicKey() {
const key = TEST_PUBLIC_KEYS[nextTestPublicKey % TEST_PUBLIC_KEYS.length];
nextTestPublicKey += 1;
return key;
}
function fakeAgent(publicKeys) {
const identities = publicKeys.map((key) => utils.parseKey(key));
return {
getIdentities(callback) {
callback(null, identities);
},
sign(_key, _data, _options, callback) {
callback(null, Buffer.from("signature"));
},
getStream(callback) {
callback(null, "forwarded-stream");
},
};
}
function getIdentities(agent) {
return new Promise((resolve, reject) => {
agent.getIdentities((error, identities) => {
if (error) reject(error);
else resolve(identities);
});
});
}
test("IdentityFile paths expand standard OpenSSH connection tokens", () => {
const resolved = resolveIdentityPath(
"%d/.ssh/key-%h-%p-%r-%u-%l-%L-%i-%%-%C",
{
hostname: "server.example.com",
port: 2222,
username: "deploy",
localHostname: "mac.example.net",
localUsername: "alice",
uid: 501,
},
);
assert.match(resolved, /key-server\.example\.com-2222-deploy-alice-mac\.example\.net-mac-501-%-[a-f0-9]{40}$/);
});
test("prepareSystemSshAgent prioritizes the identity selected by IdentityFile", async () => {
const unrelated = makePublicKey();
const selected = makePublicKey();
const agent = await prepareSystemSshAgent({
socketPath: "/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: false,
}, {
createAgent: () => fakeAgent([unrelated, selected]),
readFile: async () => `${selected} alice@mac\n`,
platform: "linux",
});
const identities = await getIdentities(agent);
assert.deepEqual(
identities.map((key) => key.getPublicSSH().toString("base64")),
[selected, unrelated].map((key) => utils.parseKey(key).getPublicSSH().toString("base64")),
);
await new Promise((resolve, reject) => {
agent.getStream((error, stream) => {
if (error) reject(error);
else {
assert.equal(stream, "forwarded-stream");
resolve();
}
});
});
});
test("prepareSystemSshAgent excludes unrelated identities for IdentitiesOnly", async () => {
const unrelated = makePublicKey();
const selected = makePublicKey();
const agent = await prepareSystemSshAgent({
socketPath: "/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: true,
}, {
createAgent: () => fakeAgent([unrelated, selected]),
readFile: async () => selected,
platform: "linux",
});
const identities = await getIdentities(agent);
assert.deepEqual(
identities.map((key) => key.getPublicSSH().toString("base64")),
[utils.parseKey(selected).getPublicSSH().toString("base64")],
);
});
test("prepareSystemSshAgent filters by a selected vault public key", async () => {
const unrelated = makePublicKey();
const selected = makePublicKey();
const agent = await prepareSystemSshAgent({
socketPath: "/tmp/agent.sock",
agentPublicKeys: [selected],
identitiesOnly: true,
}, {
createAgent: () => fakeAgent([unrelated, selected]),
platform: "linux",
});
const identities = await getIdentities(agent);
assert.deepEqual(
identities.map((key) => key.getPublicSSH().toString("base64")),
[utils.parseKey(selected).getPublicSSH().toString("base64")],
);
});
test("prepareSystemSshAgent asks macOS to load a missing configured identity from Keychain", async () => {
const selected = makePublicKey();
const sshAddCalls = [];
await prepareSystemSshAgent({
socketPath: "/private/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: true,
useKeychain: true,
addKeysToAgent: "yes",
}, {
createAgent: () => fakeAgent([]),
readFile: async () => selected,
runSshAdd: async (args) => sshAddCalls.push(args),
platform: "darwin",
});
assert.deepEqual(sshAddCalls, [[
"--apple-load-keychain",
"/Users/alice/.ssh/aws_root",
]]);
});
test("prepareSystemSshAgent does not invoke macOS Keychain loading when the identity is already present", async () => {
const selected = makePublicKey();
const sshAddCalls = [];
await prepareSystemSshAgent({
socketPath: "/private/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: true,
useKeychain: true,
addKeysToAgent: "yes",
}, {
createAgent: () => fakeAgent([selected]),
readFile: async () => selected,
runSshAdd: async (args) => sshAddCalls.push(args),
platform: "darwin",
});
assert.deepEqual(sshAddCalls, []);
});
test("prepareSystemSshAgent loads macOS Keychain when only some configured identities are present", async () => {
const first = makePublicKey();
const second = makePublicKey();
const sshAddCalls = [];
await prepareSystemSshAgent({
socketPath: "/private/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/first", "/Users/alice/.ssh/second"],
identitiesOnly: true,
useKeychain: true,
addKeysToAgent: "yes",
}, {
createAgent: () => fakeAgent([first]),
readFile: async (publicKeyPath) => publicKeyPath.endsWith("first.pub") ? first : second,
runSshAdd: async (args) => sshAddCalls.push(args),
platform: "darwin",
});
assert.deepEqual(sshAddCalls, [[
"--apple-load-keychain",
"/Users/alice/.ssh/first",
"/Users/alice/.ssh/second",
]]);
});
test("prepareSystemSshAgent does not bypass AddKeysToAgent confirmation policies", async () => {
const selected = makePublicKey();
const sshAddCalls = [];
await prepareSystemSshAgent({
socketPath: "/private/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: true,
useKeychain: true,
addKeysToAgent: "confirm",
}, {
createAgent: () => fakeAgent([]),
readFile: async () => selected,
runSshAdd: async (args) => sshAddCalls.push(args),
platform: "darwin",
});
assert.deepEqual(sshAddCalls, []);
});
test("prepareSystemSshAgent reports a clear error when strict selection has no readable public key", async () => {
await assert.rejects(
prepareSystemSshAgent({
socketPath: "/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/missing"],
identitiesOnly: true,
}, {
createAgent: () => fakeAgent([makePublicKey()]),
readFile: async () => { throw new Error("ENOENT"); },
platform: "linux",
}),
(error) => {
assert.equal(error.code, "ERR_SSH_AGENT_IDENTITY_SELECTOR_UNAVAILABLE");
assert.match(error.message, /missing\.pub/);
return true;
},
);
});
test("prepareSystemSshAgent reports every missing selector in strict multi-key mode", async () => {
const selected = makePublicKey();
await assert.rejects(
prepareSystemSshAgent({
socketPath: "/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/available", "/Users/alice/.ssh/missing"],
identitiesOnly: true,
}, {
createAgent: () => fakeAgent([selected]),
readFile: async (publicKeyPath) => {
if (publicKeyPath.endsWith("available.pub")) return selected;
throw new Error("ENOENT");
},
platform: "linux",
}),
(error) => {
assert.equal(error.code, "ERR_SSH_AGENT_IDENTITY_SELECTOR_UNAVAILABLE");
assert.match(error.message, /missing\.pub/);
return true;
},
);
});
test("prepareSystemSshAgent falls back to all identities and still invokes macOS ssh-add without a .pub file", async () => {
const loaded = makePublicKey();
const sshAddCalls = [];
const agent = await prepareSystemSshAgent({
socketPath: "/private/tmp/agent.sock",
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
identitiesOnly: false,
useKeychain: true,
addKeysToAgent: "yes",
}, {
createAgent: () => fakeAgent([loaded]),
readFile: async () => { throw new Error("ENOENT"); },
runSshAdd: async (args) => sshAddCalls.push(args),
platform: "darwin",
});
assert.deepEqual(sshAddCalls, [[
"--apple-load-keychain",
"/Users/alice/.ssh/aws_root",
]]);
assert.equal((await getIdentities(agent)).length, 1);
});