Files
ssh-dynamic-mcp/test/ssh-config-parser.test.js
Max Shcheglov b889154bb3 feat: add dynamic per-call SSH connections and secret redaction
- execute-command/upload/download accept host/port/username/password inline
  to open an ephemeral connection for a single call
- add redactSecret/redactSecrets to mask credentials in logs and errors
- logger log()/handleError() accept optional secrets[] for redaction
- add SSH_CONFIG_MISSING error code and --password-from-env CLI flag
- dynamic-mode startup: server runs with no static config
2026-09-01 10:54:20 +07:00

311 lines
9.9 KiB
JavaScript

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert';
import { lookupSshConfig } from '../build/utils/ssh-config-parser.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
let fixturesDir;
describe('SSH Config Parser', () => {
let testConfigPath;
let testConfigWithIncludePath;
let includedConfigPath;
let originalHome;
before(() => {
originalHome = process.env.HOME;
fixturesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ssh-mcp-config-test-'));
testConfigPath = path.join(fixturesDir, 'ssh-config-basic');
testConfigWithIncludePath = path.join(fixturesDir, 'ssh-config-include');
includedConfigPath = path.join(fixturesDir, 'ssh-config-included');
// 基本测试配置
fs.writeFileSync(testConfigPath, [
'# 多别名测试',
'Host dev staging',
' HostName 192.168.1.100',
' Port 2222',
' User devuser',
' IdentityFile ~/.ssh/dev_key',
'',
'# 单别名测试',
'Host prod',
' HostName 10.0.0.50',
' User produser',
' IdentityFile ~/.ssh/prod_key',
'',
'# 通配符测试',
'Host *.example.com',
' User wildcarduser',
' Port 2200',
'',
'# 全局默认值',
'Host *',
' Port 22',
' User defaultuser',
].join('\n'));
// 被包含的配置文件
fs.writeFileSync(includedConfigPath, [
'Host included-host',
' HostName 172.16.0.1',
' Port 3333',
' User includeduser',
].join('\n'));
// 带 Include 的配置文件
fs.writeFileSync(testConfigWithIncludePath, [
`Include ${includedConfigPath}`,
'',
'Host main-host',
' HostName 192.168.1.1',
' User mainuser',
'',
'Host *',
' Port 22',
].join('\n'));
});
after(() => {
process.env.HOME = originalHome;
fs.rmSync(fixturesDir, { recursive: true, force: true });
});
describe('基本功能', () => {
it('应该正确解析单个 Host 别名', () => {
const config = lookupSshConfig('prod', testConfigPath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '10.0.0.50');
assert.strictEqual(config.user, 'produser');
assert.strictEqual(config.identityFile, path.join(os.homedir(), '.ssh', 'prod_key'));
assert.strictEqual(config.port, 22); // 从 Host * fallback
});
it('应该正确解析多别名 Host 行 - dev', () => {
const config = lookupSshConfig('dev', testConfigPath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '192.168.1.100');
assert.strictEqual(config.port, 2222);
assert.strictEqual(config.user, 'devuser');
});
it('应该正确解析多别名 Host 行 - staging', () => {
const config = lookupSshConfig('staging', testConfigPath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '192.168.1.100');
assert.strictEqual(config.port, 2222);
assert.strictEqual(config.user, 'devuser');
});
it('应该支持通配符匹配', () => {
const config = lookupSshConfig('server.example.com', testConfigPath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.user, 'wildcarduser');
assert.strictEqual(config.port, 2200);
});
it('应该使用 Host * 作为默认值', () => {
const config = lookupSshConfig('unknown-host', testConfigPath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.user, 'defaultuser');
assert.strictEqual(config.port, 22);
assert.strictEqual(config.hostName, undefined);
});
it('通配符匹配应转义正则特殊字符', () => {
const tempConfig = path.join(fixturesDir, 'special-pattern-config');
fs.writeFileSync(tempConfig, [
'Host host[1]',
' User literaluser',
'',
'Host *',
' User defaultuser',
].join('\n'));
const literalConfig = lookupSshConfig('host[1]', tempConfig);
const regexLikeConfig = lookupSshConfig('host1', tempConfig);
assert.ok(literalConfig);
assert.strictEqual(literalConfig.user, 'literaluser');
assert.ok(regexLikeConfig);
assert.strictEqual(regexLikeConfig.user, 'defaultuser');
fs.unlinkSync(tempConfig);
});
it('应该支持 Host 中的否定模式', () => {
const tempConfig = path.join(fixturesDir, 'negated-pattern-config');
fs.writeFileSync(tempConfig, [
'Host *.example.com !blocked.example.com',
' User wildcarduser',
'',
'Host *',
' User defaultuser',
].join('\n'));
const allowedConfig = lookupSshConfig('app.example.com', tempConfig);
const blockedConfig = lookupSshConfig('blocked.example.com', tempConfig);
assert.ok(allowedConfig);
assert.strictEqual(allowedConfig.user, 'wildcarduser');
assert.ok(blockedConfig);
assert.strictEqual(blockedConfig.user, 'defaultuser');
fs.unlinkSync(tempConfig);
});
});
describe('Include 指令', () => {
it('应该正确处理 Include 指令', () => {
const config = lookupSshConfig('included-host', testConfigWithIncludePath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '172.16.0.1');
assert.strictEqual(config.port, 3333);
assert.strictEqual(config.user, 'includeduser');
});
it('应该在 Include 后继续解析主配置文件', () => {
const config = lookupSshConfig('main-host', testConfigWithIncludePath);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '192.168.1.1');
assert.strictEqual(config.user, 'mainuser');
assert.strictEqual(config.port, 22);
});
it('应该静默跳过不存在的 Include 文件', () => {
const tempConfig = path.join(fixturesDir, 'temp-include-config');
fs.writeFileSync(tempConfig, [
'Include /nonexistent/path/config',
'',
'Host test',
' HostName 1.2.3.4',
].join('\n'));
const config = lookupSshConfig('test', tempConfig);
assert.ok(config, '应该返回非 null 的配置');
assert.strictEqual(config.hostName, '1.2.3.4');
fs.unlinkSync(tempConfig);
});
});
describe('边界情况', () => {
it('默认配置文件不存在时应返回 null', () => {
const fakeHome = fs.mkdtempSync(path.join(fixturesDir, 'fake-home-'));
process.env.HOME = fakeHome;
try {
const config = lookupSshConfig('any-host');
assert.strictEqual(config, null);
} finally {
process.env.HOME = originalHome;
fs.rmSync(fakeHome, { recursive: true, force: true });
}
});
it('显式指定的配置文件不存在时应抛出错误', () => {
assert.throws(() => {
lookupSshConfig('any-host', '/nonexistent/config');
}, /not found/);
});
it('未找到匹配的 Host 时应返回 null', () => {
const tempConfig = path.join(fixturesDir, 'empty-config');
fs.writeFileSync(tempConfig, '# Empty config\n');
const config = lookupSshConfig('any-host', tempConfig);
assert.strictEqual(config, null);
fs.unlinkSync(tempConfig);
});
it('应该正确展开 ~ 路径', () => {
const config = lookupSshConfig('dev', testConfigPath);
assert.ok(config);
assert.ok(config.identityFile.startsWith(os.homedir()));
assert.ok(!config.identityFile.includes('~'));
});
it('应该正确处理注释行', () => {
const tempConfig = path.join(fixturesDir, 'comment-config');
fs.writeFileSync(tempConfig, [
'# 这是注释',
'Host test',
' HostName 1.2.3.4 # 行内注释',
' Port 2222',
].join('\n'));
const config = lookupSshConfig('test', tempConfig);
assert.ok(config);
assert.strictEqual(config.hostName, '1.2.3.4');
assert.strictEqual(config.port, 2222);
fs.unlinkSync(tempConfig);
});
it('应该正确处理空白行和缩进', () => {
const tempConfig = path.join(fixturesDir, 'whitespace-config');
fs.writeFileSync(tempConfig, [
'',
' Host test ',
' HostName 1.2.3.4 ',
'',
' Port 2222 ',
'',
].join('\n'));
const config = lookupSshConfig('test', tempConfig);
assert.ok(config);
assert.strictEqual(config.hostName, '1.2.3.4');
assert.strictEqual(config.port, 2222);
fs.unlinkSync(tempConfig);
});
});
describe('First-match-wins 语义', () => {
it('应该使用第一个匹配的值', () => {
const tempConfig = path.join(fixturesDir, 'priority-config');
fs.writeFileSync(tempConfig, [
'Host test',
' Port 2222',
' User firstuser',
'',
'Host test',
' Port 3333',
' User seconduser',
'',
'Host *',
' Port 22',
].join('\n'));
const config = lookupSshConfig('test', tempConfig);
assert.ok(config);
assert.strictEqual(config.port, 2222);
assert.strictEqual(config.user, 'firstuser');
fs.unlinkSync(tempConfig);
});
it('特定 Host 的值应优先于 Host *', () => {
const tempConfig = path.join(fixturesDir, 'specific-priority-config');
fs.writeFileSync(tempConfig, [
'Host specific',
' Port 2222',
'',
'Host *',
' Port 22',
' User globaluser',
].join('\n'));
const config = lookupSshConfig('specific', tempConfig);
assert.ok(config);
assert.strictEqual(config.port, 2222); // 来自 Host specific
assert.strictEqual(config.user, 'globaluser'); // 来自 Host *
fs.unlinkSync(tempConfig);
});
});
});