- 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
268 lines
8.7 KiB
JavaScript
268 lines
8.7 KiB
JavaScript
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert';
|
|
import { CommandLineParser } from '../build/cli/command-line-parser.js';
|
|
import { SSHConnectionManager } from '../build/services/ssh-connection-manager.js';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
let fixturesDir;
|
|
|
|
describe('集成测试', () => {
|
|
let originalArgv;
|
|
|
|
before(() => {
|
|
originalArgv = process.argv;
|
|
|
|
fixturesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ssh-mcp-integration-test-'));
|
|
});
|
|
|
|
after(() => {
|
|
process.argv = originalArgv;
|
|
fs.rmSync(fixturesDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('端到端场景', () => {
|
|
it('应该能够从命令行参数创建完整配置并传递给连接管理器', () => {
|
|
process.argv = [
|
|
'node',
|
|
'test',
|
|
'--host', '192.168.1.100',
|
|
'--port', '22',
|
|
'--username', 'testuser',
|
|
'--password', 'testpass',
|
|
'--whitelist', 'ls,cat,grep',
|
|
'--blacklist', 'rm,shutdown'
|
|
];
|
|
|
|
const result = CommandLineParser.parseArgs();
|
|
|
|
assert.strictEqual(result.configs.default.host, '192.168.1.100');
|
|
assert.strictEqual(result.configs.default.port, 22);
|
|
assert.strictEqual(result.configs.default.username, 'testuser');
|
|
assert.deepStrictEqual(result.configs.default.commandWhitelist, ['ls', 'cat', 'grep']);
|
|
assert.deepStrictEqual(result.configs.default.commandBlacklist, ['rm', 'shutdown']);
|
|
|
|
// 验证可以用这个配置初始化连接管理器
|
|
const manager = SSHConnectionManager.getInstance();
|
|
manager.setConfig(result.configs);
|
|
|
|
const config = manager.getConfig('default');
|
|
assert.strictEqual(config.host, '192.168.1.100');
|
|
assert.strictEqual(config.username, 'testuser');
|
|
});
|
|
|
|
it('应该能够从命令行参数生成 shell transport 配置', () => {
|
|
process.argv = [
|
|
'node',
|
|
'test',
|
|
'--host', '192.168.1.110',
|
|
'--port', '22',
|
|
'--username', 'shelluser',
|
|
'--password', 'shellpass',
|
|
'--transport-mode', 'shell',
|
|
'--shell-ready-timeout', '18000'
|
|
];
|
|
|
|
const result = CommandLineParser.parseArgs();
|
|
assert.strictEqual(result.configs.default.transportMode, 'shell');
|
|
assert.strictEqual(result.configs.default.shellReadyTimeoutMs, 18000);
|
|
|
|
const manager = SSHConnectionManager.getInstance();
|
|
manager.setConfig(result.configs);
|
|
|
|
const config = manager.getConfig('default');
|
|
assert.strictEqual(config.transportMode, 'shell');
|
|
assert.strictEqual(config.shellReadyTimeoutMs, 18000);
|
|
});
|
|
|
|
it('应该能够从 SSH config 创建完整配置', () => {
|
|
const tempSshConfig = path.join(fixturesDir, 'integration-ssh-config');
|
|
fs.writeFileSync(tempSshConfig, [
|
|
'Host integration-test',
|
|
' HostName 192.168.1.200',
|
|
' Port 2222',
|
|
' User integrationuser',
|
|
' IdentityFile ~/.ssh/integration_key',
|
|
].join('\n'));
|
|
|
|
try {
|
|
process.argv = [
|
|
'node',
|
|
'test',
|
|
'--host', 'integration-test',
|
|
'--ssh-config-file', tempSshConfig
|
|
];
|
|
|
|
const result = CommandLineParser.parseArgs();
|
|
|
|
assert.strictEqual(result.configs.default.host, '192.168.1.200');
|
|
assert.strictEqual(result.configs.default.port, 2222);
|
|
assert.strictEqual(result.configs.default.username, 'integrationuser');
|
|
assert.ok(result.configs.default.privateKey.includes('integration_key'));
|
|
|
|
// 验证可以用这个配置初始化连接管理器
|
|
const manager = SSHConnectionManager.getInstance();
|
|
manager.setConfig(result.configs);
|
|
|
|
const config = manager.getConfig('default');
|
|
assert.strictEqual(config.host, '192.168.1.200');
|
|
} finally {
|
|
fs.unlinkSync(tempSshConfig);
|
|
}
|
|
});
|
|
|
|
it('应该能够处理多服务器配置', () => {
|
|
const tempConfig = path.join(fixturesDir, 'multi-server-config.json');
|
|
fs.writeFileSync(tempConfig, JSON.stringify({
|
|
server1: {
|
|
host: '192.168.1.1',
|
|
port: 22,
|
|
username: 'user1',
|
|
password: 'pass1',
|
|
commandWhitelist: ['^ls', '^cat']
|
|
},
|
|
server2: {
|
|
host: '192.168.1.2',
|
|
port: 2222,
|
|
username: 'user2',
|
|
privateKey: '~/.ssh/key2',
|
|
commandBlacklist: ['^rm', '^shutdown']
|
|
}
|
|
}));
|
|
|
|
try {
|
|
process.argv = ['node', 'test', '--config-file', tempConfig];
|
|
|
|
const result = CommandLineParser.parseArgs();
|
|
assert.strictEqual(Object.keys(result.configs).length, 2);
|
|
|
|
const manager = SSHConnectionManager.getInstance();
|
|
manager.setConfig(result.configs);
|
|
|
|
// 验证配置列表
|
|
const allInfos = manager.getAllServerInfos();
|
|
assert.strictEqual(allInfos.length, 2);
|
|
|
|
const server1 = allInfos.find(i => i.name === 'server1');
|
|
assert.ok(server1);
|
|
assert.strictEqual(server1.host, '192.168.1.1');
|
|
assert.strictEqual(server1.connected, false);
|
|
|
|
const server2 = allInfos.find(i => i.name === 'server2');
|
|
assert.ok(server2);
|
|
assert.strictEqual(server2.port, 2222);
|
|
} finally {
|
|
fs.unlinkSync(tempConfig);
|
|
}
|
|
});
|
|
|
|
it('应该能够从配置文件生成 shell transport 配置', () => {
|
|
const tempConfig = path.join(fixturesDir, 'shell-server-config.json');
|
|
fs.writeFileSync(tempConfig, JSON.stringify({
|
|
shellbox: {
|
|
host: '192.168.1.20',
|
|
port: 22,
|
|
username: 'shelluser',
|
|
password: 'shellpass',
|
|
transportMode: 'shell',
|
|
shellReadyTimeoutMs: 12000
|
|
}
|
|
}));
|
|
|
|
try {
|
|
process.argv = ['node', 'test', '--config-file', tempConfig];
|
|
|
|
const result = CommandLineParser.parseArgs();
|
|
assert.strictEqual(result.configs.shellbox.transportMode, 'shell');
|
|
assert.strictEqual(result.configs.shellbox.shellReadyTimeoutMs, 12000);
|
|
|
|
const manager = SSHConnectionManager.getInstance();
|
|
manager.setConfig(result.configs);
|
|
|
|
const config = manager.getConfig('shellbox');
|
|
assert.strictEqual(config.transportMode, 'shell');
|
|
assert.strictEqual(config.shellReadyTimeoutMs, 12000);
|
|
} finally {
|
|
fs.unlinkSync(tempConfig);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('错误处理', () => {
|
|
it('应该正确处理无效的 JSON 配置文件', () => {
|
|
const invalidConfig = path.join(fixturesDir, 'invalid-config.json');
|
|
fs.writeFileSync(invalidConfig, '{ invalid json }');
|
|
|
|
try {
|
|
process.argv = ['node', 'test', '--config-file', invalidConfig];
|
|
assert.throws(() => {
|
|
CommandLineParser.parseArgs();
|
|
}, /Invalid JSON/);
|
|
} finally {
|
|
fs.unlinkSync(invalidConfig);
|
|
}
|
|
});
|
|
|
|
it('应该正确处理缺少必需字段的配置', () => {
|
|
const incompleteConfig = path.join(fixturesDir, 'incomplete-config.json');
|
|
fs.writeFileSync(incompleteConfig, JSON.stringify({
|
|
server1: {
|
|
host: '192.168.1.1'
|
|
// 缺少 port, username 等必需字段
|
|
}
|
|
}));
|
|
|
|
try {
|
|
process.argv = ['node', 'test', '--config-file', incompleteConfig];
|
|
assert.throws(() => {
|
|
CommandLineParser.parseArgs();
|
|
});
|
|
} finally {
|
|
fs.unlinkSync(incompleteConfig);
|
|
}
|
|
});
|
|
|
|
it('应该正确处理不存在的配置文件', () => {
|
|
process.argv = ['node', 'test', '--config-file', '/nonexistent/config.json'];
|
|
assert.throws(() => {
|
|
CommandLineParser.parseArgs();
|
|
}, /not found/);
|
|
});
|
|
|
|
it('应该正确处理缺少认证参数的情况', () => {
|
|
const emptySshConfig = path.join(fixturesDir, 'empty-ssh-config');
|
|
const originalSshAuthSock = process.env.SSH_AUTH_SOCK;
|
|
fs.writeFileSync(emptySshConfig, '');
|
|
|
|
try {
|
|
delete process.env.SSH_AUTH_SOCK;
|
|
process.argv = [
|
|
'node',
|
|
'test',
|
|
'--host', '1.2.3.4',
|
|
'--port', '22',
|
|
'--username', 'user',
|
|
'--ssh-config-file', emptySshConfig
|
|
];
|
|
assert.throws(() => {
|
|
CommandLineParser.parseArgs();
|
|
}, /Missing required parameters/);
|
|
} finally {
|
|
if (originalSshAuthSock === undefined) {
|
|
delete process.env.SSH_AUTH_SOCK;
|
|
} else {
|
|
process.env.SSH_AUTH_SOCK = originalSshAuthSock;
|
|
}
|
|
fs.unlinkSync(emptySshConfig);
|
|
}
|
|
});
|
|
|
|
it('应该正确处理无效的端口号', () => {
|
|
process.argv = ['node', 'test', '--host', '1.2.3.4', '--port', 'abc', '--username', 'user', '--password', 'pass'];
|
|
assert.throws(() => {
|
|
CommandLineParser.parseArgs();
|
|
}, /Port must be a valid number/);
|
|
});
|
|
});
|
|
});
|